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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/frontend
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js10
-rw-r--r--spec/frontend/__mocks__/lodash/debounce.js13
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js27
-rw-r--r--spec/frontend/ajax_loading_spinner_spec.js57
-rw-r--r--spec/frontend/alert_handler_spec.js46
-rw-r--r--spec/frontend/alert_management/components/alert_details_spec.js17
-rw-r--r--spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js19
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js63
-rw-r--r--spec/frontend/alert_management/components/alert_metrics_spec.js9
-rw-r--r--spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js77
-rw-r--r--spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js6
-rw-r--r--spec/frontend/alert_management/mocks/alerts.json4
-rw-r--r--spec/frontend/api_spec.js23
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js6
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js18
-rw-r--r--spec/frontend/authentication/webauthn/authenticate_spec.js132
-rw-r--r--spec/frontend/authentication/webauthn/error_spec.js50
-rw-r--r--spec/frontend/authentication/webauthn/mock_webauthn_device.js35
-rw-r--r--spec/frontend/authentication/webauthn/register_spec.js131
-rw-r--r--spec/frontend/authentication/webauthn/util.js19
-rw-r--r--spec/frontend/awards_handler_spec.js46
-rw-r--r--spec/frontend/badges/components/badge_settings_spec.js10
-rw-r--r--spec/frontend/batch_comments/mock_data.js1
-rw-r--r--spec/frontend/behaviors/autosize_spec.js18
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js15
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js18
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js21
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_embeddable_spec.js35
-rw-r--r--spec/frontend/blob/pipeline_tour_success_mock_data.js1
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js55
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js4
-rw-r--r--spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js67
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js3
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js31
-rw-r--r--spec/frontend/boards/board_list_helper.js6
-rw-r--r--spec/frontend/boards/board_list_spec.js6
-rw-r--r--spec/frontend/boards/board_new_issue_spec.js59
-rw-r--r--spec/frontend/boards/boards_store_spec.js2
-rw-r--r--spec/frontend/boards/components/board_card_layout_spec.js95
-rw-r--r--spec/frontend/boards/components/board_card_spec.js (renamed from spec/frontend/boards/board_card_spec.js)52
-rw-r--r--spec/frontend/boards/components/board_column_spec.js5
-rw-r--r--spec/frontend/boards/components/board_content_spec.js64
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js8
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js174
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js2
-rw-r--r--spec/frontend/boards/components/issuable_title_spec.js33
-rw-r--r--spec/frontend/boards/components/issue_count_spec.js2
-rw-r--r--spec/frontend/boards/components/sidebar/board_editable_item_spec.js107
-rw-r--r--spec/frontend/boards/issue_card_spec.js6
-rw-r--r--spec/frontend/boards/list_spec.js1
-rw-r--r--spec/frontend/boards/mock_data.js176
-rw-r--r--spec/frontend/boards/stores/actions_spec.js369
-rw-r--r--spec/frontend/boards/stores/getters_spec.js112
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js330
-rw-r--r--spec/frontend/branches/ajax_loading_spinner_spec.js32
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js2
-rw-r--r--spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap8
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js34
-rw-r--r--spec/frontend/clusters/components/fluentd_output_settings_spec.js2
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js11
-rw-r--r--spec/frontend/clusters/components/new_cluster_spec.js41
-rw-r--r--spec/frontend/clusters/components/uninstall_application_button_spec.js17
-rw-r--r--spec/frontend/clusters_list/components/ancestor_notice_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js7
-rw-r--r--spec/frontend/collapsed_sidebar_todo_spec.js12
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap12
-rw-r--r--spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js106
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js8
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js3
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js6
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js18
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js1
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js10
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js (renamed from spec/frontend/gl_dropdown_spec.js)54
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js19
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap21
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap14
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js80
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js22
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js20
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js57
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js41
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js158
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap40
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js3
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap24
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js14
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js18
-rw-r--r--spec/frontend/design_management/mock_data/design.js3
-rw-r--r--spec/frontend/design_management/mock_data/discussion.js45
-rw-r--r--spec/frontend/design_management/mock_data/notes.js74
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap2
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap14
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js134
-rw-r--r--spec/frontend/design_management/pages/index_apollo_spec.js162
-rw-r--r--spec/frontend/design_management/pages/index_spec.js140
-rw-r--r--spec/frontend/design_management/router_spec.js5
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js13
-rw-r--r--spec/frontend/design_management/utils/design_management_utils_spec.js17
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap42
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap104
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap115
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap68
-rw-r--r--spec/frontend/design_management_legacy/components/delete_button_spec.js51
-rw-r--r--spec/frontend/design_management_legacy/components/design_note_pin_spec.js49
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap67
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap15
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js318
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js170
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js184
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js98
-rw-r--r--spec/frontend/design_management_legacy/components/design_overlay_spec.js410
-rw-r--r--spec/frontend/design_management_legacy/components/design_presentation_spec.js553
-rw-r--r--spec/frontend/design_management_legacy/components/design_scaler_spec.js67
-rw-r--r--spec/frontend/design_management_legacy/components/design_sidebar_spec.js236
-rw-r--r--spec/frontend/design_management_legacy/components/image_spec.js133
-rw-r--r--spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap149
-rw-r--r--spec/frontend/design_management_legacy/components/list/item_spec.js169
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap61
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap28
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap29
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/index_spec.js123
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js61
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js79
-rw-r--r--spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap79
-rw-r--r--spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap455
-rw-r--r--spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap111
-rw-r--r--spec/frontend/design_management_legacy/components/upload/button_spec.js59
-rw-r--r--spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js132
-rw-r--r--spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js122
-rw-r--r--spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js14
-rw-r--r--spec/frontend/design_management_legacy/mock_data/all_versions.js8
-rw-r--r--spec/frontend/design_management_legacy/mock_data/design.js74
-rw-r--r--spec/frontend/design_management_legacy/mock_data/designs.js17
-rw-r--r--spec/frontend/design_management_legacy/mock_data/no_designs.js11
-rw-r--r--spec/frontend/design_management_legacy/mock_data/notes.js46
-rw-r--r--spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap263
-rw-r--r--spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap216
-rw-r--r--spec/frontend/design_management_legacy/pages/design/index_spec.js291
-rw-r--r--spec/frontend/design_management_legacy/pages/index_spec.js543
-rw-r--r--spec/frontend/design_management_legacy/router_spec.js82
-rw-r--r--spec/frontend/design_management_legacy/utils/cache_update_spec.js44
-rw-r--r--spec/frontend/design_management_legacy/utils/design_management_utils_spec.js176
-rw-r--r--spec/frontend/design_management_legacy/utils/error_messages_spec.js62
-rw-r--r--spec/frontend/design_management_legacy/utils/tracking_spec.js59
-rw-r--r--spec/frontend/diffs/components/app_spec.js208
-rw-r--r--spec/frontend/diffs/components/collapsed_files_warning_spec.js88
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js3
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js3
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js28
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js19
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js19
-rw-r--r--spec/frontend/diffs/components/diff_file_row_spec.js30
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js44
-rw-r--r--spec/frontend/diffs/components/diff_stats_spec.js4
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js4
-rw-r--r--spec/frontend/diffs/components/inline_diff_expansion_row_spec.js5
-rw-r--r--spec/frontend/diffs/components/inline_diff_table_row_spec.js355
-rw-r--r--spec/frontend/diffs/components/inline_diff_view_spec.js4
-rw-r--r--spec/frontend/diffs/components/merge_conflict_warning_spec.js77
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js2
-rw-r--r--spec/frontend/diffs/components/parallel_diff_table_row_spec.js259
-rw-r--r--spec/frontend/diffs/components/parallel_diff_view_spec.js40
-rw-r--r--spec/frontend/diffs/components/settings_dropdown_spec.js42
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js115
-rw-r--r--spec/frontend/diffs/mock_data/diff_file.js111
-rw-r--r--spec/frontend/diffs/mock_data/diff_metadata.js3
-rw-r--r--spec/frontend/diffs/store/actions_spec.js207
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js83
-rw-r--r--spec/frontend/diffs/store/utils_spec.js55
-rw-r--r--spec/frontend/editor/editor_lite_spec.js207
-rw-r--r--spec/frontend/editor/editor_markdown_ext_spec.js58
-rw-r--r--spec/frontend/emoji/emoji_spec.js3
-rw-r--r--spec/frontend/environments/environment_actions_spec.js17
-rw-r--r--spec/frontend/environments/environment_item_spec.js4
-rw-r--r--spec/frontend/environments/environment_monitoring_spec.js8
-rw-r--r--spec/frontend/environments/environment_pin_spec.js7
-rw-r--r--spec/frontend/environments/environment_rollback_spec.js4
-rw-r--r--spec/frontend/environments/environment_terminal_button_spec.js2
-rw-r--r--spec/frontend/environments/environments_app_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js40
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js7
-rw-r--r--spec/frontend/fixtures/search.rb6
-rw-r--r--spec/frontend/fixtures/static/ajax_loading_spinner.html3
-rw-r--r--spec/frontend/fixtures/static/deprecated_jquery_dropdown.html39
-rw-r--r--spec/frontend/fixtures/static/gl_dropdown.html26
-rw-r--r--spec/frontend/fixtures/u2f.rb4
-rw-r--r--spec/frontend/fixtures/webauthn.rb47
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js4
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js7
-rw-r--r--spec/frontend/gpg_badges_spec.js2
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap2
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js144
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js8
-rw-r--r--spec/frontend/groups/components/item_caret_spec.js4
-rw-r--r--spec/frontend/groups/components/item_stats_value_spec.js2
-rw-r--r--spec/frontend/groups/components/item_type_icon_spec.js8
-rw-r--r--spec/frontend/groups/members/index_spec.js66
-rw-r--r--spec/frontend/groups/members/mock_data.js33
-rw-r--r--spec/frontend/header_spec.js2
-rw-r--r--spec/frontend/helpers/dom_events_helper.js1
-rw-r--r--spec/frontend/helpers/fake_request_animation_frame.js1
-rw-r--r--spec/frontend/helpers/jest_helpers.js2
-rw-r--r--spec/frontend/helpers/local_storage_helper.js2
-rw-r--r--spec/frontend/helpers/local_storage_helper_spec.js9
-rw-r--r--spec/frontend/helpers/locale_helper.js2
-rw-r--r--spec/frontend/helpers/mock_apollo_helper.js23
-rw-r--r--spec/frontend/helpers/mock_dom_observer.js4
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js65
-rw-r--r--spec/frontend/ide/commit_icon_spec.js1
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js53
-rw-r--r--spec/frontend/ide/components/error_message_spec.js2
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js3
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap2
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js12
-rw-r--r--spec/frontend/ide/components/jobs/detail/scroll_button_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js8
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/list_spec.js6
-rw-r--r--spec/frontend/ide/components/merge_requests/item_spec.js2
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js36
-rw-r--r--spec/frontend/ide/components/nav_dropdown_button_spec.js2
-rw-r--r--spec/frontend/ide/components/nav_dropdown_spec.js2
-rw-r--r--spec/frontend/ide/components/new_dropdown/button_spec.js2
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js2
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js63
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js22
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js13
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js132
-rw-r--r--spec/frontend/ide/components/repo_tabs_spec.js43
-rw-r--r--spec/frontend/ide/components/terminal/session_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/terminal_controls_spec.js8
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js9
-rw-r--r--spec/frontend/ide/helpers.js1
-rw-r--r--spec/frontend/ide/lib/editor_spec.js22
-rw-r--r--spec/frontend/ide/lib/errors_spec.js70
-rw-r--r--spec/frontend/ide/lib/files_spec.js19
-rw-r--r--spec/frontend/ide/mock_data.js3
-rw-r--r--spec/frontend/ide/services/index_spec.js6
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js26
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js2
-rw-r--r--spec/frontend/ide/stores/actions_spec.js15
-rw-r--r--spec/frontend/ide/stores/getters_spec.js45
-rw-r--r--spec/frontend/ide/stores/integration_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js22
-rw-r--r--spec/frontend/ide/stores/modules/commit/mutations_spec.js21
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js40
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/mutations_spec.js12
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js2
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js18
-rw-r--r--spec/frontend/ide/sync_router_and_store_spec.js8
-rw-r--r--spec/frontend/ide/utils_spec.js162
-rw-r--r--spec/frontend/import_projects/components/bitbucket_status_table_spec.js2
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js114
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js44
-rw-r--r--spec/frontend/import_projects/components/page_query_param_sync_spec.js87
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js151
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js66
-rw-r--r--spec/frontend/import_projects/store/getters_spec.js27
-rw-r--r--spec/frontend/import_projects/store/mutations_spec.js178
-rw-r--r--spec/frontend/import_projects/utils_spec.js47
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js55
-rw-r--r--spec/frontend/incidents/mocks/incidents.json12
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap20
-rw-r--r--spec/frontend/incidents_settings/components/alerts_form_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/active_checkbox_spec.js76
-rw-r--r--spec/frontend/integrations/edit/components/active_toggle_spec.js81
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js22
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js20
-rw-r--r--spec/frontend/integrations/edit/components/override_dropdown_spec.js106
-rw-r--r--spec/frontend/integrations/edit/mock_data.js5
-rw-r--r--spec/frontend/integrations/edit/store/getters_spec.js14
-rw-r--r--spec/frontend/integrations/edit/store/state_spec.js10
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js195
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js289
-rw-r--r--spec/frontend/issuable/related_issues/components/issue_token_spec.js241
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js206
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_list_spec.js190
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js341
-rw-r--r--spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js111
-rw-r--r--spec/frontend/issuable_create/components/issuable_create_root_spec.js64
-rw-r--r--spec/frontend/issuable_create/components/issuable_form_spec.js119
-rw-r--r--spec/frontend/issuable_list/components/issuable_item_spec.js185
-rw-r--r--spec/frontend/issuable_list/components/issuable_list_root_spec.js160
-rw-r--r--spec/frontend/issuable_list/components/issuable_tabs_spec.js91
-rw-r--r--spec/frontend/issuable_list/mock_data.js135
-rw-r--r--spec/frontend/issuable_suggestions/components/app_spec.js10
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js13
-rw-r--r--spec/frontend/issue_show/components/app_spec.js122
-rw-r--r--spec/frontend/issue_show/components/description_spec.js26
-rw-r--r--spec/frontend/issue_show/components/edit_actions_spec.js21
-rw-r--r--spec/frontend/issue_show/components/incidents/highlight_bar_spec.js56
-rw-r--r--spec/frontend/issue_show/components/incidents/incident_tabs_spec.js101
-rw-r--r--spec/frontend/issue_show/helpers.js1
-rw-r--r--spec/frontend/issue_show/index_spec.js19
-rw-r--r--spec/frontend/issue_show/issue_spec.js45
-rw-r--r--spec/frontend/issue_show/mock_data.js35
-rw-r--r--spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap (renamed from spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap)0
-rw-r--r--spec/frontend/issues_list/components/issuable_spec.js (renamed from spec/frontend/issuables_list/components/issuable_spec.js)5
-rw-r--r--spec/frontend/issues_list/components/issuables_list_app_spec.js (renamed from spec/frontend/issuables_list/components/issuables_list_app_spec.js)18
-rw-r--r--spec/frontend/issues_list/components/jira_issues_list_root_spec.js (renamed from spec/frontend/issuables_list/components/issuable_list_root_app_spec.js)12
-rw-r--r--spec/frontend/issues_list/issuable_list_test_data.js (renamed from spec/frontend/issuables_list/issuable_list_test_data.js)0
-rw-r--r--spec/frontend/issues_list/service_desk_helper_spec.js28
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap4
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js6
-rw-r--r--spec/frontend/jira_import/utils/jira_import_utils_spec.js2
-rw-r--r--spec/frontend/jobs/components/artifacts_block_spec.js17
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js1
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_spec.js2
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js12
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js4
-rw-r--r--spec/frontend/jobs/store/helpers.js1
-rw-r--r--spec/frontend/labels_issue_sidebar_spec.js1
-rw-r--r--spec/frontend/lib/utils/axios_startup_calls_spec.js131
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js14
-rw-r--r--spec/frontend/lib/utils/forms_spec.js44
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js34
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js21
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js38
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js25
-rw-r--r--spec/frontend/logs/components/log_advanced_filters_spec.js3
-rw-r--r--spec/frontend/logs/components/log_control_buttons_spec.js7
-rw-r--r--spec/frontend/logs/components/log_simple_filters_spec.js5
-rw-r--r--spec/frontend/logs/mock_data.js2
-rw-r--r--spec/frontend/logs/stores/getters_spec.js48
-rw-r--r--spec/frontend/matchers.js6
-rw-r--r--spec/frontend/milestones/project_milestone_combobox_spec.js48
-rw-r--r--spec/frontend/mocks/mocks_helper.js1
-rw-r--r--spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap4
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js2
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap10
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js3
-rw-r--r--spec/frontend/monitoring/components/charts/bar_spec.js15
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js7
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js10
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js3
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js8
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js18
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js11
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js6
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js8
-rw-r--r--spec/frontend/monitoring/components/embeds/embed_group_spec.js16
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js2
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js6
-rw-r--r--spec/frontend/monitoring/fixture_data.js11
-rw-r--r--spec/frontend/monitoring/graph_data.js26
-rw-r--r--spec/frontend/monitoring/mock_data.js45
-rw-r--r--spec/frontend/mr_popover/mr_popover_spec.js2
-rw-r--r--spec/frontend/namespace_select_spec.js27
-rw-r--r--spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap2
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js38
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js9
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_resolve_button_spec.js15
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js34
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js2
-rw-r--r--spec/frontend/notes/helpers.js1
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js10
-rw-r--r--spec/frontend/notes/stores/actions_spec.js24
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap46
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap20
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap2
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap22
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap36
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap20
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap234
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap12
-rw-r--r--spec/frontend/packages/details/components/additional_metadata_spec.js2
-rw-r--r--spec/frontend/packages/details/components/app_spec.js122
-rw-r--r--spec/frontend/packages/details/components/composer_installation_spec.js15
-rw-r--r--spec/frontend/packages/details/components/conan_installation_spec.js2
-rw-r--r--spec/frontend/packages/details/components/maven_installation_spec.js2
-rw-r--r--spec/frontend/packages/details/components/npm_installation_spec.js6
-rw-r--r--spec/frontend/packages/details/components/nuget_installation_spec.js2
-rw-r--r--spec/frontend/packages/details/components/package_history_spec.js6
-rw-r--r--spec/frontend/packages/details/components/package_title_spec.js99
-rw-r--r--spec/frontend/packages/details/store/actions_spec.js24
-rw-r--r--spec/frontend/packages/details/store/getters_spec.js4
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap2
-rw-r--r--spec/frontend/packages/list/components/packages_list_app_spec.js49
-rw-r--r--spec/frontend/packages/list/components/packages_list_spec.js4
-rw-r--r--spec/frontend/packages/list/components/packages_sort_spec.js4
-rw-r--r--spec/frontend/packages/list/stores/actions_spec.js3
-rw-r--r--spec/frontend/packages/mock_data.js3
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap178
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap18
-rw-r--r--spec/frontend/packages/shared/components/package_list_row_spec.js22
-rw-r--r--spec/frontend/packages/shared/components/packages_list_loader_spec.js33
-rw-r--r--spec/frontend/packages/shared/components/publish_method_spec.js6
-rw-r--r--spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap11
-rw-r--r--spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js66
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js1
-rw-r--r--spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js2
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js4
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js8
-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/project_feature_settings_spec.js4
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js2
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js2
-rw-r--r--spec/frontend/performance_bar/components/request_warning_spec.js2
-rw-r--r--spec/frontend/performance_bar/index_spec.js2
-rw-r--r--spec/frontend/performance_bar/services/performance_bar_service_spec.js43
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js106
-rw-r--r--spec/frontend/pipeline_new/mock_data.js12
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js3
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js7
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js68
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js75
-rw-r--r--spec/frontend/pipelines/pipeline_graph/gitlab_ci_yaml_visualization_spec.js47
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js80
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js59
-rw-r--r--spec/frontend/pipelines/pipeline_graph/utils_spec.js150
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/stores/utils_spec.js26
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js11
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js12
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_spec.js3
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js23
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js17
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js32
-rw-r--r--spec/frontend/profile/account/components/delete_account_modal_spec.js82
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js19
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap8
-rw-r--r--spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap4
-rw-r--r--spec/frontend/projects/settings/access_dropdown_spec.js1
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js2
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js18
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js32
-rw-r--r--spec/frontend/registry/explorer/components/delete_button_spec.js1
-rw-r--r--spec/frontend/registry/explorer/components/details_page/details_header_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_spec.js1
-rw-r--r--spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js63
-rw-r--r--spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/list_page/registry_header_spec.js134
-rw-r--r--spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js2
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js2
-rw-r--r--spec/frontend/registry/explorer/stubs.js2
-rw-r--r--spec/frontend/registry/shared/mocks.js1
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap113
-rw-r--r--spec/frontend/releases/components/app_index_spec.js141
-rw-r--r--spec/frontend/releases/components/app_show_spec.js2
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js36
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js7
-rw-r--r--spec/frontend/releases/components/release_block_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_pagination_graphql_spec.js175
-rw-r--r--spec/frontend/releases/components/releases_pagination_rest_spec.js72
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js52
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js7
-rw-r--r--spec/frontend/releases/mock_data.js128
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js150
-rw-r--r--spec/frontend/releases/stores/modules/list/helpers.js1
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js4
-rw-r--r--spec/frontend/releases/util_spec.js55
-rw-r--r--spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js11
-rw-r--r--spec/frontend/reports/accessibility_report/mock_data.js1
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js45
-rw-r--r--spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap6
-rw-r--r--spec/frontend/reports/components/grouped_issues_list_spec.js2
-rw-r--r--spec/frontend/reports/components/grouped_test_reports_app_spec.js13
-rw-r--r--spec/frontend/repository/components/table/index_spec.js28
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js35
-rw-r--r--spec/frontend/repository/components/web_ide_link_spec.js51
-rw-r--r--spec/frontend/repository/log_tree_spec.js43
-rw-r--r--spec/frontend/repository/router_spec.js13
-rw-r--r--spec/frontend/search/components/state_filter_spec.js104
-rw-r--r--spec/frontend/search_autocomplete_spec.js3
-rw-r--r--spec/frontend/serverless/utils.js1
-rw-r--r--spec/frontend/shortcuts_spec.js85
-rw-r--r--spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap40
-rw-r--r--spec/frontend/sidebar/__snapshots__/todo_spec.js.snap2
-rw-r--r--spec/frontend/sidebar/assignees_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/severity/severity_spec.js57
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js166
-rw-r--r--spec/frontend/sidebar/issuable_assignees_spec.js59
-rw-r--r--spec/frontend/sidebar/participants_spec.js10
-rw-r--r--spec/frontend/sidebar/sidebar_labels_spec.js124
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js6
-rw-r--r--spec/frontend/sidebar/subscriptions_spec.js2
-rw-r--r--spec/frontend/sidebar/todo_spec.js11
-rw-r--r--spec/frontend/snippet/snippet_bundle_spec.js8
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap6
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap8
-rw-r--r--spec/frontend/snippets/components/edit_spec.js23
-rw-r--r--spec/frontend/snippets/components/embed_dropdown_spec.js70
-rw-r--r--spec/frontend/snippets/components/show_spec.js12
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js17
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js118
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js89
-rw-r--r--spec/frontend/static_site_editor/components/edit_drawer_spec.js68
-rw-r--r--spec/frontend/static_site_editor/components/front_matter_controls_spec.js78
-rw-r--r--spec/frontend/static_site_editor/components/publish_toolbar_spec.js17
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/file_spec.js2
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js2
-rw-r--r--spec/frontend/static_site_editor/mock_data.js17
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js2
-rw-r--r--spec/frontend/static_site_editor/services/formatter_spec.js15
-rw-r--r--spec/frontend/static_site_editor/services/load_source_content_spec.js7
-rw-r--r--spec/frontend/static_site_editor/services/parse_source_file_spec.js62
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js2
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js202
-rw-r--r--spec/frontend/tooltips/index_spec.js149
-rw-r--r--spec/frontend/tracking_spec.js22
-rw-r--r--spec/frontend/vue_mr_widget/components/mock_data.js1
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js10
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js329
-rw-r--r--spec/frontend/vue_mr_widget/components/review_app_link_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js21
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js1
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap12
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js203
-rw-r--r--spec/frontend/vue_shared/components/alert_detail_table_spec.js74
-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.js4
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js138
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js203
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js73
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js207
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js106
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js102
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js190
-rw-r--r--spec/frontend/vue_shared/components/icon_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js47
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/ordered_layout_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap60
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap (renamed from spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap)8
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js (renamed from spec/frontend/packages/details/components/code_instruction_spec.js)35
-rw-r--r--spec/frontend/vue_shared/components/registry/details_row_spec.js (renamed from spec/frontend/registry/shared/components/details_row_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/registry/history_item_spec.js (renamed from spec/frontend/packages/details/components/history_element_spec.js)14
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js (renamed from spec/frontend/registry/explorer/components/list_item_spec.js)81
-rw-r--r--spec/frontend/vue_shared/components/registry/metadata_item_spec.js101
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js98
-rw-r--r--spec/frontend/vue_shared/components/remove_member_modal_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap739
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js87
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js40
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js70
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js46
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/todo_button_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js106
-rw-r--r--spec/frontend/whats_new/components/app_spec.js24
-rw-r--r--spec/frontend/whats_new/components/trigger_spec.js43
608 files changed, 16244 insertions, 12029 deletions
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index ea36f1dabaf..237f8b408f5 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -18,8 +18,16 @@ jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
}));
jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
+ props: ['target', 'id', 'triggers', 'placement', 'container', 'boundary', 'disabled'],
render(h) {
- return h('div', this.$attrs, this.$slots.default);
+ return h(
+ 'div',
+ {
+ class: 'gl-tooltip',
+ ...this.$attrs,
+ },
+ this.$slots.default,
+ );
},
}));
diff --git a/spec/frontend/__mocks__/lodash/debounce.js b/spec/frontend/__mocks__/lodash/debounce.js
index 97fdb39097a..e8b61c80147 100644
--- a/spec/frontend/__mocks__/lodash/debounce.js
+++ b/spec/frontend/__mocks__/lodash/debounce.js
@@ -8,4 +8,15 @@
// [2]: https://gitlab.com/gitlab-org/gitlab/-/issues/213378
// Further reference: https://github.com/facebook/jest/issues/3465
-export default fn => fn;
+export default fn => {
+ const debouncedFn = jest.fn().mockImplementation(fn);
+ debouncedFn.cancel = jest.fn();
+ debouncedFn.flush = jest.fn().mockImplementation(() => {
+ const errorMessage =
+ "The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'.";
+
+ throw new Error(errorMessage);
+ });
+
+ return debouncedFn;
+};
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 6904e34db5d..1a3b151afa0 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
@@ -78,7 +78,7 @@ describe('AddContextCommitsModal', () => {
findSearch().vm.$emit('input', searchText);
expect(searchCommits).not.toBeCalled();
jest.advanceTimersByTime(500);
- expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText, undefined);
+ expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText);
});
it('disabled ok button when no row is selected', () => {
@@ -119,18 +119,17 @@ describe('AddContextCommitsModal', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
- expect(createContextCommits).toHaveBeenCalledWith(
- expect.anything(),
- { commits: [{ ...commit, isSelected: true }], forceReload: true },
- undefined,
- );
+ expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
+ commits: [{ ...commit, isSelected: true }],
+ forceReload: true,
+ });
});
});
it('"removeContextCommits" when only added commits are to be removed ', () => {
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
- expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true, undefined);
+ expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true);
});
});
it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', () => {
@@ -138,12 +137,10 @@ describe('AddContextCommitsModal', () => {
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
- expect(createContextCommits).toHaveBeenCalledWith(
- expect.anything(),
- { commits: [{ ...commit, isSelected: true }] },
- undefined,
- );
- expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
+ expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
+ commits: [{ ...commit, isSelected: true }],
+ });
+ expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined);
});
});
});
@@ -156,7 +153,7 @@ describe('AddContextCommitsModal', () => {
});
it('"resetModalState" to reset all the modal state', () => {
findModal().vm.$emit('cancel');
- expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
+ expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined);
});
});
@@ -168,7 +165,7 @@ describe('AddContextCommitsModal', () => {
});
it('"resetModalState" to reset all the modal state', () => {
findModal().vm.$emit('close');
- expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
+ expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined);
});
});
});
diff --git a/spec/frontend/ajax_loading_spinner_spec.js b/spec/frontend/ajax_loading_spinner_spec.js
deleted file mode 100644
index 8ed2ee49ff8..00000000000
--- a/spec/frontend/ajax_loading_spinner_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'jquery';
-import AjaxLoadingSpinner from '~/ajax_loading_spinner';
-
-describe('Ajax Loading Spinner', () => {
- const fixtureTemplate = 'static/ajax_loading_spinner.html';
- preloadFixtures(fixtureTemplate);
-
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- AjaxLoadingSpinner.init();
- });
-
- it('change current icon with spinner icon and disable link while waiting ajax response', done => {
- jest.spyOn($, 'ajax').mockImplementation(req => {
- const xhr = new XMLHttpRequest();
- const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
- const icon = ajaxLoadingSpinner.querySelector('i');
-
- req.beforeSend(xhr, { dataType: 'text/html' });
-
- expect(icon).not.toHaveClass('fa-trash-o');
- expect(icon).toHaveClass('fa-spinner');
- expect(icon).toHaveClass('fa-spin');
- expect(icon.dataset.icon).toEqual('fa-trash-o');
- expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
-
- req.complete({});
-
- done();
- const deferred = $.Deferred();
- return deferred.promise();
- });
- document.querySelector('.js-ajax-loading-spinner').click();
- });
-
- it('use original icon again and enabled the link after complete the ajax request', done => {
- jest.spyOn($, 'ajax').mockImplementation(req => {
- const xhr = new XMLHttpRequest();
- const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
-
- req.beforeSend(xhr, { dataType: 'text/html' });
- req.complete({});
-
- const icon = ajaxLoadingSpinner.querySelector('i');
-
- expect(icon).toHaveClass('fa-trash-o');
- expect(icon).not.toHaveClass('fa-spinner');
- expect(icon).not.toHaveClass('fa-spin');
- expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
-
- done();
- const deferred = $.Deferred();
- return deferred.promise();
- });
- document.querySelector('.js-ajax-loading-spinner').click();
- });
-});
diff --git a/spec/frontend/alert_handler_spec.js b/spec/frontend/alert_handler_spec.js
new file mode 100644
index 00000000000..ba2f4f24aa5
--- /dev/null
+++ b/spec/frontend/alert_handler_spec.js
@@ -0,0 +1,46 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import initAlertHandler from '~/alert_handler';
+
+describe('Alert Handler', () => {
+ const ALERT_SELECTOR = 'gl-alert';
+ const CLOSE_SELECTOR = 'gl-alert-dismiss';
+ const ALERT_HTML = `<div class="${ALERT_SELECTOR}"><button class="${CLOSE_SELECTOR}">Dismiss</button></div>`;
+
+ const findFirstAlert = () => document.querySelector(`.${ALERT_SELECTOR}`);
+ const findAllAlerts = () => document.querySelectorAll(`.${ALERT_SELECTOR}`);
+ const findFirstCloseButton = () => document.querySelector(`.${CLOSE_SELECTOR}`);
+
+ describe('initAlertHandler', () => {
+ describe('with one alert', () => {
+ beforeEach(() => {
+ setHTMLFixture(ALERT_HTML);
+ initAlertHandler();
+ });
+
+ it('should render the alert', () => {
+ expect(findFirstAlert()).toExist();
+ });
+
+ it('should dismiss the alert on click', () => {
+ findFirstCloseButton().click();
+ expect(findFirstAlert()).not.toExist();
+ });
+ });
+
+ describe('with two alerts', () => {
+ beforeEach(() => {
+ setHTMLFixture(ALERT_HTML + ALERT_HTML);
+ initAlertHandler();
+ });
+
+ it('should render two alerts', () => {
+ expect(findAllAlerts()).toHaveLength(2);
+ });
+
+ it('should dismiss only one alert on click', () => {
+ findFirstCloseButton().click();
+ expect(findAllAlerts()).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/alert_management/components/alert_details_spec.js b/spec/frontend/alert_management/components/alert_details_spec.js
index 2c4ed100a56..8aa26dbca3b 100644
--- a/spec/frontend/alert_management/components/alert_details_spec.js
+++ b/spec/frontend/alert_management/components/alert_details_spec.js
@@ -1,7 +1,8 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertDetails from '~/alert_management/components/alert_details.vue';
import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
@@ -22,8 +23,6 @@ describe('AlertDetails', () => {
const projectId = '1';
const $router = { replace: jest.fn() };
- const findDetailsTable = () => wrapper.find(GlTable);
-
function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
wrapper = mountMethod(AlertDetails, {
provide: {
@@ -66,6 +65,7 @@ describe('AlertDetails', () => {
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findViewIncidentBtn = () => wrapper.find('[data-testid="viewIncidentBtn"]');
const findIncidentCreationAlert = () => wrapper.find('[data-testid="incidentCreationError"]');
+ const findDetailsTable = () => wrapper.find(AlertDetailsTable);
describe('Alert details', () => {
describe('when alert is null', () => {
@@ -87,8 +87,8 @@ describe('AlertDetails', () => {
expect(wrapper.find('[data-testid="overview"]').exists()).toBe(true);
});
- it('renders a tab with full alert information', () => {
- expect(wrapper.find('[data-testid="fullDetails"]').exists()).toBe(true);
+ it('renders a tab with an activity feed', () => {
+ expect(wrapper.find('[data-testid="activity"]').exists()).toBe(true);
});
it('renders severity', () => {
@@ -198,7 +198,6 @@ describe('AlertDetails', () => {
mountComponent({ data: { alert: mockAlert } });
});
it('should display a table of raw alert details data', () => {
- wrapper.find('[data-testid="fullDetails"]').trigger('click');
expect(findDetailsTable().exists()).toBe(true);
});
});
@@ -234,7 +233,7 @@ describe('AlertDetails', () => {
describe('header', () => {
const findHeader = () => wrapper.find('[data-testid="alert-header"]');
- const stubs = { TimeAgoTooltip: '<span>now</span>' };
+ const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } };
describe('individual header fields', () => {
describe.each`
@@ -268,8 +267,8 @@ describe('AlertDetails', () => {
it.each`
index | tabId
${0} | ${'overview'}
- ${1} | ${'fullDetails'}
- ${2} | ${'metrics'}
+ ${1} | ${'metrics'}
+ ${2} | ${'activity'}
`('will navigate to the correct tab via $tabId', ({ index, tabId }) => {
wrapper.setData({ currentTabIndex: index });
expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
index 2814b5ce357..ea7b4584a63 100644
--- a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue';
-import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.mutation.graphql';
+import createAlertTodoMutation from '~/alert_management/graphql/mutations/alert_todo_create.mutation.graphql';
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -61,14 +62,14 @@ describe('Alert Details Sidebar To Do', () => {
expect(findToDoButton().text()).toBe('Add a To-Do');
});
- it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
+ it('calls `$apollo.mutate` with `createAlertTodoMutation` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
findToDoButton().trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: AlertMarkTodo,
+ mutation: createAlertTodoMutation,
variables: {
iid: '1527542',
projectPath: 'projectPath',
@@ -76,6 +77,7 @@ describe('Alert Details Sidebar To Do', () => {
});
});
});
+
describe('removing a todo', () => {
beforeEach(() => {
mountComponent({
@@ -91,12 +93,19 @@ describe('Alert Details Sidebar To Do', () => {
expect(findToDoButton().text()).toBe('Mark as done');
});
- it('calls `$apollo.mutate` with `AlertMarkTodoDone` mutation and variables containing `id`', async () => {
+ it('calls `$apollo.mutate` with `todoMarkDoneMutation` mutation and variables containing `id`', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
findToDoButton().trigger('click');
await wrapper.vm.$nextTick();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: todoMarkDoneMutation,
+ update: expect.anything(),
+ variables: {
+ id: '1234',
+ },
+ });
});
});
});
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index 5dd0d9dc1ba..bcad415eb19 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -11,6 +11,7 @@ import {
GlBadge,
GlPagination,
GlSearchBoxByType,
+ GlAvatar,
} from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -39,19 +40,21 @@ describe('AlertManagementTable', () => {
const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo);
const findFirstStatusOption = () => findStatusDropdown().find(GlDeprecatedDropdownItem);
- const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]');
- const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
- const findSeverityColumnHeader = () => wrapper.findAll('th').at(0);
const findPagination = () => wrapper.find(GlPagination);
const findSearch = () => wrapper.find(GlSearchBoxByType);
+ const findSeverityColumnHeader = () =>
+ wrapper.find('[data-testid="alert-management-severity-sort"]');
+ const findFirstIDField = () => wrapper.findAll('[data-testid="idField"]').at(0);
+ const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]');
+ const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
const findIssueFields = () => wrapper.findAll('[data-testid="issueField"]');
const findAlertError = () => wrapper.find('[data-testid="alert-error"]');
const alertsCount = {
- open: 14,
- triggered: 10,
- acknowledged: 6,
- resolved: 1,
- all: 16,
+ open: 24,
+ triggered: 20,
+ acknowledged: 16,
+ resolved: 11,
+ all: 26,
};
const selectFirstStatusOption = () => {
findFirstStatusOption().vm.$emit('click');
@@ -92,13 +95,10 @@ describe('AlertManagementTable', () => {
});
}
- beforeEach(() => {
- mountComponent({ data: { alerts: mockAlerts, alertsCount } });
- });
-
afterEach(() => {
if (wrapper) {
wrapper.destroy();
+ wrapper = null;
}
});
@@ -192,6 +192,17 @@ describe('AlertManagementTable', () => {
).toContain('gl-hover-bg-blue-50');
});
+ it('displays the alert ID and title formatted correctly', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
+ loading: false,
+ });
+
+ expect(findFirstIDField().exists()).toBe(true);
+ expect(findFirstIDField().text()).toBe(`#${mockAlerts[0].iid} ${mockAlerts[0].title}`);
+ });
+
it('displays status dropdown', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
@@ -207,7 +218,11 @@ describe('AlertManagementTable', () => {
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
- expect(findStatusDropdown().contains('.dropdown-title')).toBe(false);
+ expect(
+ findStatusDropdown()
+ .find('.dropdown-title')
+ .exists(),
+ ).toBe(false);
});
it('shows correct severity icons', () => {
@@ -255,18 +270,22 @@ describe('AlertManagementTable', () => {
).toBe('Unassigned');
});
- it('renders username(s) when assignee(s) present', () => {
+ it('renders user avatar when assignee present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
- expect(
- findAssignees()
- .at(1)
- .text(),
- ).toBe(mockAlerts[1].assignees.nodes[0].username);
+ const avatar = findAssignees()
+ .at(1)
+ .find(GlAvatar);
+ const { src, label } = avatar.attributes();
+ const { name, avatarUrl } = mockAlerts[1].assignees.nodes[0];
+
+ expect(avatar.exists()).toBe(true);
+ expect(label).toBe(name);
+ expect(src).toBe(avatarUrl);
});
it('navigates to the detail page when alert row is clicked', () => {
@@ -502,7 +521,11 @@ describe('AlertManagementTable', () => {
await selectFirstStatusOption();
expect(findAlertError().exists()).toBe(true);
- expect(findAlertError().contains('[data-testid="htmlError"]')).toBe(true);
+ expect(
+ findAlertError()
+ .find('[data-testid="htmlError"]')
+ .exists(),
+ ).toBe(true);
});
});
diff --git a/spec/frontend/alert_management/components/alert_metrics_spec.js b/spec/frontend/alert_management/components/alert_metrics_spec.js
index e0a069fa1a8..42da8c3768b 100644
--- a/spec/frontend/alert_management/components/alert_metrics_spec.js
+++ b/spec/frontend/alert_management/components/alert_metrics_spec.js
@@ -3,15 +3,13 @@ import waitForPromises from 'helpers/wait_for_promises';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import AlertMetrics from '~/alert_management/components/alert_metrics.vue';
+import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
jest.mock('~/monitoring/stores', () => ({
monitoringDashboard: {},
}));
-const mockEmbedName = 'MetricsEmbedStub';
-
jest.mock('~/monitoring/components/embeds/metric_embed.vue', () => ({
- name: mockEmbedName,
render(h) {
return h('div');
},
@@ -26,13 +24,10 @@ describe('Alert Metrics', () => {
propsData: {
...props,
},
- stubs: {
- MetricEmbed: true,
- },
});
}
- const findChart = () => wrapper.find({ name: mockEmbedName });
+ const findChart = () => wrapper.find(MetricEmbed);
const findEmptyState = () => wrapper.find({ ref: 'emptyState' });
afterEach(() => {
diff --git a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
index a14596b6722..4c9db02eff4 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
+++ b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
@@ -56,6 +56,9 @@ describe('Alert Details Sidebar Assignees', () => {
mock.restore();
});
+ const findAssigned = () => wrapper.find('[data-testid="assigned-users"]');
+ const findUnassigned = () => wrapper.find('[data-testid="unassigned-users"]');
+
describe('updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
@@ -100,32 +103,30 @@ describe('Alert Details Sidebar Assignees', () => {
});
});
- it('renders a unassigned option', () => {
+ it('renders a unassigned option', async () => {
wrapper.setData({ isDropdownSearching: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDeprecatedDropdownItem).text()).toBe('Unassigned');
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find(GlDeprecatedDropdownItem).text()).toBe('Unassigned');
});
- it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', () => {
+ it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
wrapper.setData({ isDropdownSearching: false });
- return wrapper.vm.$nextTick().then(() => {
- wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
+ await wrapper.vm.$nextTick();
+ wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: AlertSetAssignees,
- variables: {
- iid: '1527542',
- assigneeUsernames: ['root'],
- projectPath: 'projectPath',
- },
- });
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: AlertSetAssignees,
+ variables: {
+ iid: '1527542',
+ assigneeUsernames: ['root'],
+ projectPath: 'projectPath',
+ },
});
});
- it('shows an error when request contains error messages', () => {
+ it('emits an error when request contains error messages', () => {
wrapper.setData({ isDropdownSearching: false });
const errorMutationResult = {
data: {
@@ -137,18 +138,48 @@ describe('Alert Details Sidebar Assignees', () => {
};
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult);
-
- return wrapper.vm.$nextTick().then(() => {
- const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0);
- SideBarAssigneeItem.vm.$emit('click');
- expect(wrapper.emitted('alert-refresh')).toBeUndefined();
- });
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0);
+ SideBarAssigneeItem.vm.$emit('update-alert-assignees');
+ })
+ .then(() => {
+ expect(wrapper.emitted('alert-error')).toBeDefined();
+ });
});
it('stops updating and cancels loading when the request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
wrapper.vm.updateAlertAssignees('root');
- expect(wrapper.find('[data-testid="unassigned-users"]').text()).toBe('assign yourself');
+ expect(findUnassigned().text()).toBe('assign yourself');
+ });
+
+ it('shows a user avatar, username and full name when a user is set', () => {
+ mountComponent({
+ data: { alert: mockAlerts[1] },
+ sidebarCollapsed: false,
+ loading: false,
+ stubs: {
+ SidebarAssignee,
+ },
+ });
+
+ expect(
+ findAssigned()
+ .find('img')
+ .attributes('src'),
+ ).toBe('/url');
+ expect(
+ findAssigned()
+ .find('.dropdown-menu-user-full-name')
+ .text(),
+ ).toBe('root');
+ expect(
+ findAssigned()
+ .find('.dropdown-menu-user-username')
+ .text(),
+ ).toBe('root');
});
});
});
diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
index 5bd0d3b3c17..a8fe40687e1 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
@@ -56,7 +56,11 @@ describe('Alert Details Sidebar Status', () => {
});
it('displays the dropdown status header', () => {
- expect(findStatusDropdown().contains('.dropdown-title')).toBe(true);
+ expect(
+ findStatusDropdown()
+ .find('.dropdown-title')
+ .exists(),
+ ).toBe(true);
});
describe('updating the alert status', () => {
diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json
index fec101a52b4..5267a4fe50d 100644
--- a/spec/frontend/alert_management/mocks/alerts.json
+++ b/spec/frontend/alert_management/mocks/alerts.json
@@ -20,7 +20,7 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "ACKNOWLEDGED",
- "assignees": { "nodes": [{ "username": "root" }] },
+ "assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] },
"issueIid": "1",
"notes": {
"nodes": [
@@ -49,7 +49,7 @@
"startedAt": "2020-04-17T23:18:14.996Z",
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "RESOLVED",
- "assignees": { "nodes": [{ "username": "root" }] },
+ "assignees": { "nodes": [{ "username": "root", "avatarUrl": "/url", "name": "root" }] },
"notes": {
"nodes": [
{
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 4f4de62c229..3ae0d06162d 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -398,6 +398,29 @@ describe('Api', () => {
});
});
+ describe('projectMilestones', () => {
+ it('fetches project milestones', done => {
+ const projectId = 1;
+ const options = { state: 'active' };
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/milestones`;
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ id: 1,
+ title: 'milestone1',
+ state: 'active',
+ },
+ ]);
+
+ Api.projectMilestones(projectId, options)
+ .then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].title).toBe('milestone1');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('newLabel', () => {
it('creates a new label', done => {
const namespace = 'some namespace';
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
index 8abef2ae1b2..7a87b420195 100644
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ b/spec/frontend/authentication/u2f/authenticate_spec.js
@@ -76,7 +76,7 @@ describe('U2FAuthenticate', () => {
describe('errors', () => {
it('displays an error message', () => {
- const setupButton = container.find('#js-login-u2f-device');
+ const setupButton = container.find('#js-login-2fa-device');
setupButton.trigger('click');
u2fDevice.respondToAuthenticateRequest({
errorCode: 'error!',
@@ -87,14 +87,14 @@ describe('U2FAuthenticate', () => {
});
it('allows retrying authentication after an error', () => {
- let setupButton = container.find('#js-login-u2f-device');
+ let setupButton = container.find('#js-login-2fa-device');
setupButton.trigger('click');
u2fDevice.respondToAuthenticateRequest({
errorCode: 'error!',
});
const retryButton = container.find('#js-token-2fa-try-again');
retryButton.trigger('click');
- setupButton = container.find('#js-login-u2f-device');
+ setupButton = container.find('#js-login-2fa-device');
setupButton.trigger('click');
u2fDevice.respondToAuthenticateRequest({
deviceData: 'this is data from the device',
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
index 3c2ecdbba66..e89ef773be6 100644
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ b/spec/frontend/authentication/u2f/register_spec.js
@@ -13,8 +13,8 @@ describe('U2FRegister', () => {
beforeEach(done => {
loadFixtures('u2f/register.html');
u2fDevice = new MockU2FDevice();
- container = $('#js-register-u2f');
- component = new U2FRegister(container, $('#js-register-u2f-templates'), {}, 'token');
+ container = $('#js-register-token-2fa');
+ component = new U2FRegister(container, {});
component
.start()
.then(done)
@@ -22,9 +22,9 @@ describe('U2FRegister', () => {
});
it('allows registering a U2F device', () => {
- const setupButton = container.find('#js-setup-u2f-device');
+ const setupButton = container.find('#js-setup-token-2fa-device');
- expect(setupButton.text()).toBe('Set up new U2F device');
+ expect(setupButton.text()).toBe('Set up new device');
setupButton.trigger('click');
const inProgressMessage = container.children('p');
@@ -41,7 +41,7 @@ describe('U2FRegister', () => {
describe('errors', () => {
it("doesn't allow the same device to be registered twice (for the same user", () => {
- const setupButton = container.find('#js-setup-u2f-device');
+ const setupButton = container.find('#js-setup-token-2fa-device');
setupButton.trigger('click');
u2fDevice.respondToRegisterRequest({
errorCode: 4,
@@ -52,7 +52,7 @@ describe('U2FRegister', () => {
});
it('displays an error message for other errors', () => {
- const setupButton = container.find('#js-setup-u2f-device');
+ const setupButton = container.find('#js-setup-token-2fa-device');
setupButton.trigger('click');
u2fDevice.respondToRegisterRequest({
errorCode: 'error!',
@@ -63,14 +63,14 @@ describe('U2FRegister', () => {
});
it('allows retrying registration after an error', () => {
- let setupButton = container.find('#js-setup-u2f-device');
+ let setupButton = container.find('#js-setup-token-2fa-device');
setupButton.trigger('click');
u2fDevice.respondToRegisterRequest({
errorCode: 'error!',
});
- const retryButton = container.find('#U2FTryAgain');
+ const retryButton = container.find('#js-token-2fa-try-again');
retryButton.trigger('click');
- setupButton = container.find('#js-setup-u2f-device');
+ setupButton = container.find('#js-setup-token-2fa-device');
setupButton.trigger('click');
u2fDevice.respondToRegisterRequest({
deviceData: 'this is data from the device',
diff --git a/spec/frontend/authentication/webauthn/authenticate_spec.js b/spec/frontend/authentication/webauthn/authenticate_spec.js
new file mode 100644
index 00000000000..0a82adfd0ee
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/authenticate_spec.js
@@ -0,0 +1,132 @@
+import $ from 'jquery';
+import waitForPromises from 'helpers/wait_for_promises';
+import WebAuthnAuthenticate from '~/authentication/webauthn/authenticate';
+import MockWebAuthnDevice from './mock_webauthn_device';
+import { useMockNavigatorCredentials } from './util';
+
+const mockResponse = {
+ type: 'public-key',
+ id: '',
+ rawId: '',
+ response: { clientDataJSON: '', authenticatorData: '', signature: '', userHandle: '' },
+ getClientExtensionResults: () => {},
+};
+
+describe('WebAuthnAuthenticate', () => {
+ preloadFixtures('webauthn/authenticate.html');
+ useMockNavigatorCredentials();
+
+ let fallbackElement;
+ let webAuthnDevice;
+ let container;
+ let component;
+ let submitSpy;
+
+ const findDeviceResponseInput = () => container[0].querySelector('#js-device-response');
+ const findDeviceResponseInputValue = () => findDeviceResponseInput().value;
+ const findMessage = () => container[0].querySelector('p');
+ const findRetryButton = () => container[0].querySelector('#js-token-2fa-try-again');
+ const expectAuthenticated = () => {
+ expect(container.text()).toMatchInterpolatedText(
+ 'We heard back from your device. You have been authenticated.',
+ );
+ expect(findDeviceResponseInputValue()).toBe(JSON.stringify(mockResponse));
+ expect(submitSpy).toHaveBeenCalled();
+ };
+
+ beforeEach(() => {
+ loadFixtures('webauthn/authenticate.html');
+ fallbackElement = document.createElement('div');
+ fallbackElement.classList.add('js-2fa-form');
+ webAuthnDevice = new MockWebAuthnDevice();
+ container = $('#js-authenticate-token-2fa');
+ component = new WebAuthnAuthenticate(
+ container,
+ '#js-login-token-2fa-form',
+ {
+ options:
+ // we need some valid base64 for base64ToBuffer
+ // so we use "YQ==" = base64("a")
+ JSON.stringify({
+ challenge: 'YQ==',
+ timeout: 120000,
+ allowCredentials: [
+ { type: 'public-key', id: 'YQ==' },
+ { type: 'public-key', id: 'YQ==' },
+ ],
+ userVerification: 'discouraged',
+ }),
+ },
+ document.querySelector('#js-login-2fa-device'),
+ fallbackElement,
+ );
+ submitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit');
+ });
+
+ describe('with webauthn unavailable', () => {
+ let oldGetCredentials;
+
+ beforeEach(() => {
+ oldGetCredentials = window.navigator.credentials.get;
+ window.navigator.credentials.get = null;
+ });
+
+ afterEach(() => {
+ window.navigator.credentials.get = oldGetCredentials;
+ });
+
+ it('falls back to normal 2fa', () => {
+ component.start();
+
+ expect(container.html()).toBe('');
+ expect(container[0]).toHaveClass('hidden');
+ expect(fallbackElement).not.toHaveClass('hidden');
+ });
+ });
+
+ describe('with webauthn available', () => {
+ beforeEach(() => {
+ component.start();
+ });
+
+ it('shows in progress', () => {
+ const inProgressMessage = container.find('p');
+
+ expect(inProgressMessage.text()).toMatchInterpolatedText(
+ "Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.",
+ );
+ });
+
+ it('allows authenticating via a WebAuthn device', () => {
+ webAuthnDevice.respondToAuthenticateRequest(mockResponse);
+
+ return waitForPromises().then(() => {
+ expectAuthenticated();
+ });
+ });
+
+ describe('errors', () => {
+ beforeEach(() => {
+ webAuthnDevice.rejectAuthenticateRequest(new DOMException());
+
+ return waitForPromises();
+ });
+
+ it('displays an error message', () => {
+ expect(submitSpy).not.toHaveBeenCalled();
+ expect(findMessage().textContent).toMatchInterpolatedText(
+ 'There was a problem communicating with your device. (Error)',
+ );
+ });
+
+ it('allows retrying authentication after an error', () => {
+ findRetryButton().click();
+ webAuthnDevice.respondToAuthenticateRequest(mockResponse);
+
+ return waitForPromises().then(() => {
+ expectAuthenticated();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/webauthn/error_spec.js b/spec/frontend/authentication/webauthn/error_spec.js
new file mode 100644
index 00000000000..26f1ca5e27d
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/error_spec.js
@@ -0,0 +1,50 @@
+import WebAuthnError from '~/authentication/webauthn/error';
+
+describe('WebAuthnError', () => {
+ it.each([
+ [
+ 'NotSupportedError',
+ 'Your device is not compatible with GitLab. Please try another device',
+ 'authenticate',
+ ],
+ ['InvalidStateError', 'This device has not been registered with us.', 'authenticate'],
+ ['InvalidStateError', 'This device has already been registered with us.', 'register'],
+ ['UnknownError', 'There was a problem communicating with your device.', 'register'],
+ ])('exception %s will have message %s, flow type: %s', (exception, expectedMessage, flowType) => {
+ expect(new WebAuthnError(new DOMException('', exception), flowType).message()).toEqual(
+ expectedMessage,
+ );
+ });
+
+ describe('SecurityError', () => {
+ const { location } = window;
+
+ beforeEach(() => {
+ delete window.location;
+ window.location = {};
+ });
+
+ afterEach(() => {
+ window.location = location;
+ });
+
+ it('returns a descriptive error if https is disabled', () => {
+ window.location.protocol = 'http:';
+
+ const expectedMessage =
+ 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ expect(
+ new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
+ ).toEqual(expectedMessage);
+ });
+
+ it('returns a generic error if https is enabled', () => {
+ window.location.protocol = 'https:';
+
+ const expectedMessage = 'There was a problem communicating with your device.';
+ expect(
+ new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
+ ).toEqual(expectedMessage);
+ });
+ });
+});
diff --git a/spec/frontend/authentication/webauthn/mock_webauthn_device.js b/spec/frontend/authentication/webauthn/mock_webauthn_device.js
new file mode 100644
index 00000000000..39df94df46b
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/mock_webauthn_device.js
@@ -0,0 +1,35 @@
+/* eslint-disable no-unused-expressions */
+
+export default class MockWebAuthnDevice {
+ constructor() {
+ this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
+ this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
+ window.navigator.credentials || (window.navigator.credentials = {});
+ window.navigator.credentials.create = () =>
+ new Promise((resolve, reject) => {
+ this.registerCallback = resolve;
+ this.registerRejectCallback = reject;
+ });
+ window.navigator.credentials.get = () =>
+ new Promise((resolve, reject) => {
+ this.authenticateCallback = resolve;
+ this.authenticateRejectCallback = reject;
+ });
+ }
+
+ respondToRegisterRequest(params) {
+ return this.registerCallback(params);
+ }
+
+ respondToAuthenticateRequest(params) {
+ return this.authenticateCallback(params);
+ }
+
+ rejectRegisterRequest(params) {
+ return this.registerRejectCallback(params);
+ }
+
+ rejectAuthenticateRequest(params) {
+ return this.authenticateRejectCallback(params);
+ }
+}
diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js
new file mode 100644
index 00000000000..1de952d176d
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/register_spec.js
@@ -0,0 +1,131 @@
+import $ from 'jquery';
+import waitForPromises from 'helpers/wait_for_promises';
+import WebAuthnRegister from '~/authentication/webauthn/register';
+import MockWebAuthnDevice from './mock_webauthn_device';
+import { useMockNavigatorCredentials } from './util';
+
+describe('WebAuthnRegister', () => {
+ preloadFixtures('webauthn/register.html');
+ useMockNavigatorCredentials();
+
+ const mockResponse = {
+ type: 'public-key',
+ id: '',
+ rawId: '',
+ response: {
+ clientDataJSON: '',
+ attestationObject: '',
+ },
+ getClientExtensionResults: () => {},
+ };
+ let webAuthnDevice;
+ let container;
+ let component;
+
+ beforeEach(() => {
+ loadFixtures('webauthn/register.html');
+ webAuthnDevice = new MockWebAuthnDevice();
+ container = $('#js-register-token-2fa');
+ component = new WebAuthnRegister(container, {
+ options: {
+ rp: '',
+ user: {
+ id: '',
+ name: '',
+ displayName: '',
+ },
+ challenge: '',
+ pubKeyCredParams: '',
+ },
+ });
+ component.start();
+ });
+
+ const findSetupButton = () => container.find('#js-setup-token-2fa-device');
+ const findMessage = () => container.find('p');
+ const findDeviceResponse = () => container.find('#js-device-response');
+ const findRetryButton = () => container.find('#js-token-2fa-try-again');
+
+ it('shows setup button', () => {
+ expect(findSetupButton().text()).toBe('Set up new device');
+ });
+
+ describe('when unsupported', () => {
+ const { location, PublicKeyCredential } = window;
+
+ beforeEach(() => {
+ delete window.location;
+ delete window.credentials;
+ window.location = {};
+ window.PublicKeyCredential = undefined;
+ });
+
+ afterEach(() => {
+ window.location = location;
+ window.PublicKeyCredential = PublicKeyCredential;
+ });
+
+ it.each`
+ httpsEnabled | expectedText
+ ${false} | ${'WebAuthn only works with HTTPS-enabled websites'}
+ ${true} | ${'Please use a supported browser, e.g. Chrome (67+) or Firefox'}
+ `('when https is $httpsEnabled', ({ httpsEnabled, expectedText }) => {
+ window.location.protocol = httpsEnabled ? 'https:' : 'http:';
+ component.start();
+
+ expect(findMessage().text()).toContain(expectedText);
+ });
+ });
+
+ describe('when setup', () => {
+ beforeEach(() => {
+ findSetupButton().trigger('click');
+ });
+
+ it('shows in progress message', () => {
+ expect(findMessage().text()).toContain('Trying to communicate with your device');
+ });
+
+ it('registers device', () => {
+ webAuthnDevice.respondToRegisterRequest(mockResponse);
+
+ return waitForPromises().then(() => {
+ expect(findMessage().text()).toContain('Your device was successfully set up!');
+ expect(findDeviceResponse().val()).toBe(JSON.stringify(mockResponse));
+ });
+ });
+
+ it.each`
+ errorName | expectedText
+ ${'NotSupportedError'} | ${'Your device is not compatible with GitLab'}
+ ${'NotAllowedError'} | ${'There was a problem communicating with your device'}
+ `('when fails with $errorName', ({ errorName, expectedText }) => {
+ webAuthnDevice.rejectRegisterRequest(new DOMException('', errorName));
+
+ return waitForPromises().then(() => {
+ expect(findMessage().text()).toContain(expectedText);
+ expect(findRetryButton().length).toBe(1);
+ });
+ });
+
+ it('can retry', () => {
+ webAuthnDevice.respondToRegisterRequest({
+ errorCode: 'error!',
+ });
+
+ return waitForPromises()
+ .then(() => {
+ findRetryButton().click();
+
+ expect(findMessage().text()).toContain('Trying to communicate with your device');
+
+ webAuthnDevice.respondToRegisterRequest(mockResponse);
+ return waitForPromises();
+ })
+ .then(() => {
+ expect(findMessage().text()).toContain('Your device was successfully set up!');
+ expect(findDeviceResponse().val()).toBe(JSON.stringify(mockResponse));
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/webauthn/util.js b/spec/frontend/authentication/webauthn/util.js
new file mode 100644
index 00000000000..d8f5a67ee1f
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/util.js
@@ -0,0 +1,19 @@
+export function useMockNavigatorCredentials() {
+ let oldNavigatorCredentials;
+ let oldPublicKeyCredential;
+
+ beforeEach(() => {
+ oldNavigatorCredentials = navigator.credentials;
+ oldPublicKeyCredential = window.PublicKeyCredential;
+ navigator.credentials = {
+ get: jest.fn(),
+ create: jest.fn(),
+ };
+ window.PublicKeyCredential = function MockPublicKeyCredential() {};
+ });
+
+ afterEach(() => {
+ navigator.credentials = oldNavigatorCredentials;
+ window.PublicKeyCredential = oldPublicKeyCredential;
+ });
+}
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index 1a1738ecf4a..f0ed18248f0 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -4,7 +4,6 @@ import MockAdapter from 'axios-mock-adapter';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import axios from '~/lib/utils/axios_utils';
import loadAwardsHandler from '~/awards_handler';
-import { setTestTimeout } from './helpers/timeout';
import { EMOJI_VERSION } from '~/emoji';
window.gl = window.gl || {};
@@ -17,7 +16,44 @@ const urlRoot = gon.relative_url_root;
describe('AwardsHandler', () => {
useFakeRequestAnimationFrame();
- const emojiData = getJSONFixture('emojis/emojis.json');
+ const emojiData = {
+ '8ball': {
+ c: 'activity',
+ e: '🎱',
+ d: 'billiards',
+ u: '6.0',
+ },
+ grinning: {
+ c: 'people',
+ e: '😀',
+ d: 'grinning face',
+ u: '6.1',
+ },
+ angel: {
+ c: 'people',
+ e: '👼',
+ d: 'baby angel',
+ u: '6.0',
+ },
+ anger: {
+ c: 'symbols',
+ e: '💢',
+ d: 'anger symbol',
+ u: '6.0',
+ },
+ alien: {
+ c: 'people',
+ e: '👽',
+ d: 'extraterrestrial alien',
+ u: '6.0',
+ },
+ sunglasses: {
+ c: 'people',
+ e: '😎',
+ d: 'smiling face with sunglasses',
+ u: '6.0',
+ },
+ };
preloadFixtures('snippets/show.html');
const openAndWaitForEmojiMenu = (sel = '.js-add-award') => {
@@ -25,7 +61,7 @@ describe('AwardsHandler', () => {
.eq(0)
.click();
- jest.advanceTimersByTime(200);
+ jest.runOnlyPendingTimers();
const $menu = $('.emoji-menu');
@@ -37,10 +73,6 @@ describe('AwardsHandler', () => {
};
beforeEach(async () => {
- // These tests have had some timeout issues
- // https://gitlab.com/gitlab-org/gitlab/-/issues/221086
- setTestTimeout(6000);
-
mock = new MockAdapter(axios);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index 8c3f1ea2749..b6a86746598 100644
--- a/spec/frontend/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -82,14 +82,14 @@ describe('BadgeSettings component', () => {
const form = vm.$el.querySelector('form:nth-of-type(1)');
expect(form).not.toBe(null);
- const submitButton = form.querySelector('.btn-success');
-
- expect(submitButton).not.toBe(null);
- expect(submitButton).toHaveText(/Save changes/);
- const cancelButton = form.querySelector('.btn-cancel');
+ const cancelButton = form.querySelector('[data-testid="cancelEditing"]');
expect(cancelButton).not.toBe(null);
expect(cancelButton).toHaveText(/Cancel/);
+ const submitButton = form.querySelector('[data-testid="saveEditing"]');
+
+ expect(submitButton).not.toBe(null);
+ expect(submitButton).toHaveText(/Save changes/);
});
it('displays no badge list', () => {
diff --git a/spec/frontend/batch_comments/mock_data.js b/spec/frontend/batch_comments/mock_data.js
index 5601e489066..973f0d469d4 100644
--- a/spec/frontend/batch_comments/mock_data.js
+++ b/spec/frontend/batch_comments/mock_data.js
@@ -1,6 +1,5 @@
import { TEST_HOST } from 'spec/test_constants';
-// eslint-disable-next-line import/prefer-default-export
export const createDraft = () => ({
author: {
id: 1,
diff --git a/spec/frontend/behaviors/autosize_spec.js b/spec/frontend/behaviors/autosize_spec.js
index 59abae479d4..3444c7b4075 100644
--- a/spec/frontend/behaviors/autosize_spec.js
+++ b/spec/frontend/behaviors/autosize_spec.js
@@ -1,20 +1,24 @@
-import $ from 'jquery';
import '~/behaviors/autosize';
function load() {
- $(document).trigger('load');
+ document.dispatchEvent(new Event('DOMContentLoaded'));
}
+jest.mock('~/helpers/startup_css_helper', () => {
+ return {
+ waitForCSSLoaded: jest.fn().mockImplementation(cb => cb.apply()),
+ };
+});
+
describe('Autosize behavior', () => {
beforeEach(() => {
- setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>');
+ setFixtures('<textarea class="js-autosize"></textarea>');
});
- it('does not overwrite the resize property', () => {
+ it('is applied to the textarea', () => {
load();
- expect($('textarea')).toHaveCss({
- resize: 'vertical',
- });
+ const textarea = document.querySelector('textarea');
+ expect(textarea.classList).toContain('js-autosize-initialized');
});
});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index ef6b1673b7c..46b4e5d3d5c 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -10,7 +10,20 @@ jest.mock('~/emoji/support');
describe('gl_emoji', () => {
let mock;
- const emojiData = getJSONFixture('emojis/emojis.json');
+ const emojiData = {
+ grey_question: {
+ c: 'symbols',
+ e: '❔',
+ d: 'white question mark ornament',
+ u: '6.0',
+ },
+ bomb: {
+ c: 'objects',
+ e: '💣',
+ d: 'bomb',
+ u: '6.0',
+ },
+ };
beforeAll(() => {
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 9232a709194..3db95e5ad3f 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.contains(GlLoadingIcon)).toBe(true);
- expect(wrapper.contains(BlobContentError)).toBe(false);
- expect(wrapper.contains(RichViewer)).toBe(false);
- expect(wrapper.contains(SimpleViewer)).toBe(false);
+ 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);
});
it('renders error if there is any in the viewer', () => {
const renderError = 'Oops';
const viewer = { ...SimpleViewerMock, renderError };
createComponent({}, viewer);
- expect(wrapper.contains(GlLoadingIcon)).toBe(false);
- expect(wrapper.contains(BlobContentError)).toBe(true);
- expect(wrapper.contains(RichViewer)).toBe(false);
- expect(wrapper.contains(SimpleViewer)).toBe(false);
+ 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);
});
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.contains(viewer)).toBe(true);
+ expect(wrapper.find(viewer).exists()).toBe(true);
},
);
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js
index 3cc210e972c..dbed086a552 100644
--- a/spec/frontend/blob/components/blob_edit_content_spec.js
+++ b/spec/frontend/blob/components/blob_edit_content_spec.js
@@ -2,12 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BlobEditContent from '~/blob/components/blob_edit_content.vue';
import * as utils from '~/blob/utils';
-import Editor from '~/editor/editor_lite';
jest.mock('~/editor/editor_lite');
describe('Blob Header Editing', () => {
let wrapper;
+ const onDidChangeModelContent = jest.fn();
+ const updateModelLanguage = jest.fn();
+ const getValue = jest.fn();
const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
const fileName = 'lorem.txt';
const fileGlobalId = 'snippet_777';
@@ -24,7 +26,12 @@ describe('Blob Header Editing', () => {
}
beforeEach(() => {
- jest.spyOn(utils, 'initEditorLite');
+ jest.spyOn(utils, 'initEditorLite').mockImplementation(() => ({
+ onDidChangeModelContent,
+ updateModelLanguage,
+ getValue,
+ dispose: jest.fn(),
+ }));
createComponent();
});
@@ -34,8 +41,8 @@ describe('Blob Header Editing', () => {
});
const triggerChangeContent = val => {
- jest.spyOn(Editor.prototype, 'getValue').mockReturnValue(val);
- const [cb] = Editor.prototype.onChangeContent.mock.calls[0];
+ getValue.mockReturnValue(val);
+ const [cb] = onDidChangeModelContent.mock.calls[0];
cb();
@@ -58,7 +65,7 @@ describe('Blob Header Editing', () => {
createComponent({ value: undefined });
expect(spy).not.toHaveBeenCalled();
- expect(wrapper.contains('#editor')).toBe(true);
+ expect(wrapper.find('#editor').exists()).toBe(true);
});
it('initialises Editor Lite', () => {
@@ -79,12 +86,12 @@ describe('Blob Header Editing', () => {
});
return nextTick().then(() => {
- expect(Editor.prototype.updateModelLanguage).toHaveBeenCalledWith(newFileName);
+ expect(updateModelLanguage).toHaveBeenCalledWith(newFileName);
});
});
it('registers callback with editor onChangeContent', () => {
- expect(Editor.prototype.onChangeContent).toHaveBeenCalledWith(expect.any(Function));
+ expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
});
it('emits input event when the blob content is changed', () => {
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index c71595a79cf..4355f46db7e 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -31,7 +31,7 @@ describe('Blob Header Editing', () => {
});
it('contains a form input field', () => {
- expect(wrapper.contains(GlFormInput)).toBe(true);
+ expect(wrapper.find(GlFormInput).exists()).toBe(true);
});
it('does not show delete button', () => {
diff --git a/spec/frontend/blob/components/blob_embeddable_spec.js b/spec/frontend/blob/components/blob_embeddable_spec.js
deleted file mode 100644
index 1f6790013ca..00000000000
--- a/spec/frontend/blob/components/blob_embeddable_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlFormInputGroup } from '@gitlab/ui';
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
-
-describe('Blob Embeddable', () => {
- let wrapper;
- const url = 'https://foo.bar';
-
- function createComponent() {
- wrapper = shallowMount(BlobEmbeddable, {
- propsData: {
- url,
- },
- });
- }
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders gl-form-input-group component', () => {
- expect(wrapper.find(GlFormInputGroup).exists()).toBe(true);
- });
-
- it('makes up optionValues based on the url prop', () => {
- expect(wrapper.vm.optionValues).toEqual([
- { name: 'Embed', value: expect.stringContaining(`${url}.js`) },
- { name: 'Share', value: url },
- ]);
- });
-});
diff --git a/spec/frontend/blob/pipeline_tour_success_mock_data.js b/spec/frontend/blob/pipeline_tour_success_mock_data.js
index 7819fcce85d..9dea3969d63 100644
--- a/spec/frontend/blob/pipeline_tour_success_mock_data.js
+++ b/spec/frontend/blob/pipeline_tour_success_mock_data.js
@@ -1,5 +1,6 @@
const modalProps = {
goToPipelinesPath: 'some_pipeline_path',
+ projectMergeRequestsPath: 'some_mr_path',
commitCookie: 'some_cookie',
humanAccess: 'maintainer',
};
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index 9998cd7f91c..50db1675e13 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -10,10 +10,7 @@ describe('PipelineTourSuccessModal', () => {
let cookieSpy;
let trackingSpy;
- beforeEach(() => {
- document.body.dataset.page = 'projects:blob:show';
- trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
-
+ const createComponent = () => {
wrapper = shallowMount(pipelineTourSuccess, {
propsData: modalProps,
stubs: {
@@ -21,13 +18,49 @@ describe('PipelineTourSuccessModal', () => {
GlSprintf,
},
});
+ };
+ beforeEach(() => {
+ document.body.dataset.page = 'projects:blob:show';
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
cookieSpy = jest.spyOn(Cookies, 'remove');
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
unmockTracking();
+ Cookies.remove(modalProps.commitCookie);
+ });
+
+ describe('when the commitCookie contains the mr path', () => {
+ const expectedMrPath = 'expected_mr_path';
+
+ beforeEach(() => {
+ Cookies.set(modalProps.commitCookie, expectedMrPath);
+ createComponent();
+ });
+
+ it('renders the path from the commit cookie for back to the merge request button', () => {
+ const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' });
+
+ expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
+ });
+ });
+
+ describe('when the commitCookie does not contain mr path', () => {
+ const expectedMrPath = modalProps.projectMergeRequestsPath;
+
+ beforeEach(() => {
+ Cookies.set(modalProps.commitCookie, true);
+ createComponent();
+ });
+
+ it('renders the path from projectMergeRequestsPath for back to the merge request button', () => {
+ const goToMrBtn = wrapper.find({ ref: 'goToMergeRequest' });
+
+ expect(goToMrBtn.attributes('href')).toBe(expectedMrPath);
+ });
});
it('has expected structure', () => {
@@ -58,7 +91,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: 'goto' });
+ const goToBtn = wrapper.find({ ref: 'goToPipelines' });
triggerEvent(goToBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
@@ -67,5 +100,17 @@ describe('PipelineTourSuccessModal', () => {
value: '10',
});
});
+
+ 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' });
+ triggerEvent(goToBtn.element);
+
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
+ label: 'congratulate_first_pipeline',
+ property: modalProps.humanAccess,
+ value: '20',
+ });
+ });
});
});
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 4714d34dbec..e55b8e4af24 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
@@ -16,6 +16,7 @@ const commitTrackLabel = 'suggest_commit_first_project_gitlab_ci_yml';
const dismissCookie = 'suggest_gitlab_ci_yml_99';
const humanAccess = 'owner';
+const mergeRequestPath = '/some/path';
describe('Suggest gitlab-ci.yml Popover', () => {
let wrapper;
@@ -26,10 +27,11 @@ describe('Suggest gitlab-ci.yml Popover', () => {
target,
trackLabel,
dismissKey,
+ mergeRequestPath,
humanAccess,
},
stubs: {
- 'gl-popover': '<div><slot name="title"></slot><slot></slot></div>',
+ 'gl-popover': { template: '<div><slot name="title"></slot><slot></slot></div>' },
},
});
}
diff --git a/spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js b/spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js
new file mode 100644
index 00000000000..8dc71f99010
--- /dev/null
+++ b/spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js
@@ -0,0 +1,67 @@
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlAlert } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import WebIdeAlert from '~/blob/suggest_web_ide_ci/components/web_ide_alert.vue';
+
+const dismissEndpoint = '/-/user_callouts';
+const featureId = 'web_ide_alert_dismissed';
+const editPath = 'edit/master/-/.gitlab-ci.yml';
+
+describe('WebIdeAlert', () => {
+ let wrapper;
+ let mock;
+
+ const findButton = () => wrapper.find(GlButton);
+ const findAlert = () => wrapper.find(GlAlert);
+ const dismissAlert = alertWrapper => alertWrapper.vm.$emit('dismiss');
+ const getPostPayload = () => JSON.parse(mock.history.post[0].data);
+
+ const createComponent = () => {
+ wrapper = shallowMount(WebIdeAlert, {
+ propsData: {
+ dismissEndpoint,
+ featureId,
+ editPath,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onPost(dismissEndpoint).reply(200);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ mock.restore();
+ });
+
+ describe('with defaults', () => {
+ it('displays alert correctly', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('web ide button link has correct path', () => {
+ expect(findButton().attributes('href')).toBe(editPath);
+ });
+
+ it('dismisses alert correctly', async () => {
+ const alertWrapper = findAlert();
+
+ dismissAlert(alertWrapper);
+
+ await waitForPromises();
+
+ expect(alertWrapper.exists()).toBe(false);
+ expect(mock.history.post).toHaveLength(1);
+ expect(getPostPayload()).toEqual({ feature_name: featureId });
+ });
+ });
+});
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index 98fa96de124..a105b62586b 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -43,7 +43,8 @@ describe('BlobBundle', () => {
data-target="#target"
data-track-label="suggest_gitlab_ci_yml"
data-dismiss-key="1"
- data-human-access="owner">
+ data-human-access="owner"
+ data-merge-request-path="path/to/mr">
<button id='commit-changes' class="js-commit-button"></button>
<a class="btn btn-cancel" href="#"></a>
</div>
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 9642b55b9b4..8f92e8498b9 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -1,15 +1,18 @@
import EditBlob from '~/blob_edit/edit_blob';
import EditorLite from '~/editor/editor_lite';
import MarkdownExtension from '~/editor/editor_markdown_ext';
+import FileTemplateExtension from '~/editor/editor_file_template_ext';
jest.mock('~/editor/editor_lite');
jest.mock('~/editor/editor_markdown_ext');
describe('Blob Editing', () => {
+ const mockInstance = 'foo';
beforeEach(() => {
setFixtures(
- `<div class="js-edit-blob-form"><div id="file_path"></div><div id="iditor"></div><input id="file-content"></div>`,
+ `<div class="js-edit-blob-form"><div id="file_path"></div><div id="editor"></div><input id="file-content"></div>`,
);
+ jest.spyOn(EditorLite.prototype, 'createInstance').mockReturnValue(mockInstance);
});
const initEditor = (isMarkdown = false) => {
@@ -19,13 +22,29 @@ describe('Blob Editing', () => {
});
};
- it('does not load MarkdownExtension by default', async () => {
+ it('loads FileTemplateExtension by default', async () => {
await initEditor();
- expect(EditorLite.prototype.use).not.toHaveBeenCalled();
+ expect(EditorLite.prototype.use).toHaveBeenCalledWith(
+ expect.arrayContaining([FileTemplateExtension]),
+ mockInstance,
+ );
});
- it('loads MarkdownExtension only for the markdown files', async () => {
- await initEditor(true);
- expect(EditorLite.prototype.use).toHaveBeenCalledWith(MarkdownExtension);
+ describe('Markdown', () => {
+ it('does not load MarkdownExtension by default', async () => {
+ await initEditor();
+ expect(EditorLite.prototype.use).not.toHaveBeenCalledWith(
+ expect.arrayContaining([MarkdownExtension]),
+ mockInstance,
+ );
+ });
+
+ it('loads MarkdownExtension only for the markdown files', async () => {
+ await initEditor(true);
+ expect(EditorLite.prototype.use).toHaveBeenCalledWith(
+ [MarkdownExtension, FileTemplateExtension],
+ mockInstance,
+ );
+ });
});
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index b51a82f2a35..80d7a72151d 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -52,10 +52,12 @@ export default function createComponent({
list,
issues: list.issues,
loading: false,
- issueLinkBase: '/issues',
- rootPath: '/',
...componentProps,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
}).$mount();
Vue.nextTick(() => {
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 3a64b004847..88883ae61d4 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -45,10 +45,12 @@ const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listP
list,
issues: list.issues,
loading: false,
- issueLinkBase: '/issues',
- rootPath: '/',
...componentProps,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
}).$mount();
Vue.nextTick(() => {
diff --git a/spec/frontend/boards/board_new_issue_spec.js b/spec/frontend/boards/board_new_issue_spec.js
index 94afc8a2b45..3eebfeca965 100644
--- a/spec/frontend/boards/board_new_issue_spec.js
+++ b/spec/frontend/boards/board_new_issue_spec.js
@@ -1,6 +1,7 @@
/* global List */
import Vue from 'vue';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import boardNewIssue from '~/boards/components/board_new_issue.vue';
@@ -10,6 +11,7 @@ import '~/boards/models/list';
import { listObj, boardsMockInterceptor } from './mock_data';
describe('Issue boards new issue form', () => {
+ let wrapper;
let vm;
let list;
let mock;
@@ -24,13 +26,11 @@ describe('Issue boards new issue form', () => {
const dummySubmitEvent = {
preventDefault() {},
};
- vm.$refs.submitButton = vm.$el.querySelector('.btn-success');
- return vm.submit(dummySubmitEvent);
+ wrapper.vm.$refs.submitButton = wrapper.find({ ref: 'submitButton' });
+ return wrapper.vm.submit(dummySubmitEvent);
};
beforeEach(() => {
- setFixtures('<div class="test-container"></div>');
-
const BoardNewIssueComp = Vue.extend(boardNewIssue);
mock = new MockAdapter(axios);
@@ -43,46 +43,52 @@ describe('Issue boards new issue form', () => {
newIssueMock = Promise.resolve(promiseReturn);
jest.spyOn(list, 'newIssue').mockImplementation(() => newIssueMock);
- vm = new BoardNewIssueComp({
+ wrapper = mount(BoardNewIssueComp, {
propsData: {
+ disabled: false,
list,
},
- }).$mount(document.querySelector('.test-container'));
+ provide: {
+ groupId: null,
+ },
+ });
+
+ vm = wrapper.vm;
return Vue.nextTick();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
mock.restore();
});
it('calls submit if submit button is clicked', () => {
- jest.spyOn(vm, 'submit').mockImplementation(e => e.preventDefault());
+ jest.spyOn(wrapper.vm, 'submit').mockImplementation();
vm.title = 'Testing Title';
- return Vue.nextTick().then(() => {
- vm.$el.querySelector('.btn-success').click();
-
- expect(vm.submit.mock.calls.length).toBe(1);
- });
+ return Vue.nextTick()
+ .then(submitIssue)
+ .then(() => {
+ expect(wrapper.vm.submit).toHaveBeenCalled();
+ });
});
it('disables submit button if title is empty', () => {
- expect(vm.$el.querySelector('.btn-success').disabled).toBe(true);
+ expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(true);
});
it('enables submit button if title is not empty', () => {
- vm.title = 'Testing Title';
+ wrapper.setData({ title: 'Testing Title' });
return Vue.nextTick().then(() => {
- expect(vm.$el.querySelector('.form-control').value).toBe('Testing Title');
- expect(vm.$el.querySelector('.btn-success').disabled).not.toBe(true);
+ expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title');
+ expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(false);
});
});
it('clears title after clicking cancel', () => {
- vm.$el.querySelector('.btn-default').click();
+ wrapper.find({ ref: 'cancelButton' }).trigger('click');
return Vue.nextTick().then(() => {
expect(vm.title).toBe('');
@@ -97,7 +103,7 @@ describe('Issue boards new issue form', () => {
describe('submit success', () => {
it('creates new issue', () => {
- vm.title = 'submit title';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -107,17 +113,18 @@ describe('Issue boards new issue form', () => {
});
it('enables button after submit', () => {
- vm.title = 'submit issue';
+ jest.spyOn(wrapper.vm, 'submit').mockImplementation();
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
.then(() => {
- expect(vm.$el.querySelector('.btn-success').disabled).toBe(false);
+ expect(wrapper.vm.$refs.submitButton.props().disabled).toBe(false);
});
});
it('clears title after submit', () => {
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -128,7 +135,7 @@ describe('Issue boards new issue form', () => {
it('sets detail issue after submit', () => {
expect(boardsStore.detail.issue.title).toBe(undefined);
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -138,7 +145,7 @@ describe('Issue boards new issue form', () => {
});
it('sets detail list after submit', () => {
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -149,7 +156,7 @@ describe('Issue boards new issue form', () => {
it('sets detail weight after submit', () => {
boardsStore.weightFeatureAvailable = true;
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
@@ -160,7 +167,7 @@ describe('Issue boards new issue form', () => {
it('does not set detail weight after submit', () => {
boardsStore.weightFeatureAvailable = false;
- vm.title = 'submit issue';
+ wrapper.setData({ title: 'submit issue' });
return Vue.nextTick()
.then(submitIssue)
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 29cc8f981bd..41971137b95 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -312,7 +312,7 @@ describe('boardsStore', () => {
});
describe('newIssue', () => {
- const id = 'not-creative';
+ const id = 1;
const issue = { some: 'issue data' };
const url = `${endpoints.listsEndpoint}/${id}/issues`;
const expectedRequest = expect.objectContaining({
diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js
new file mode 100644
index 00000000000..80f649a1a96
--- /dev/null
+++ b/spec/frontend/boards/components/board_card_layout_spec.js
@@ -0,0 +1,95 @@
+/* global List */
+/* global ListLabel */
+
+import { shallowMount } from '@vue/test-utils';
+
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+
+import '~/boards/models/label';
+import '~/boards/models/assignee';
+import '~/boards/models/list';
+import store from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
+import BoardCardLayout from '~/boards/components/board_card_layout.vue';
+import issueCardInner from '~/boards/components/issue_card_inner.vue';
+import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
+
+describe('Board card layout', () => {
+ let wrapper;
+ let mock;
+ let list;
+
+ // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
+ const mountComponent = propsData => {
+ wrapper = shallowMount(BoardCardLayout, {
+ stubs: {
+ issueCardInner,
+ },
+ store,
+ propsData: {
+ list,
+ issue: list.issues[0],
+ disabled: false,
+ index: 0,
+ ...propsData,
+ },
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
+ });
+ };
+
+ const setupData = () => {
+ list = new List(listObj);
+ boardsStore.create();
+ boardsStore.detail.issue = {};
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: '#000cff',
+ text_color: 'white',
+ description: 'test',
+ });
+ return waitForPromises().then(() => {
+ list.issues[0].labels.push(label1);
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ setMockEndpoints();
+ return setupData();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ list = null;
+ mock.restore();
+ });
+
+ describe('mouse events', () => {
+ it('sets showDetail to true on mousedown', async () => {
+ mountComponent();
+
+ wrapper.trigger('mousedown');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.showDetail).toBe(true);
+ });
+
+ it('sets showDetail to false on mousemove', async () => {
+ mountComponent();
+ wrapper.trigger('mousedown');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.showDetail).toBe(true);
+ wrapper.trigger('mousemove');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.showDetail).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/boards/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index d01b895f996..a3ddcdf01b7 100644
--- a/spec/frontend/boards/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -2,7 +2,7 @@
/* global ListAssignee */
/* global ListLabel */
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
@@ -15,12 +15,12 @@ import '~/boards/models/assignee';
import '~/boards/models/list';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
-import boardCard from '~/boards/components/board_card.vue';
+import BoardCard from '~/boards/components/board_card.vue';
import issueCardInner from '~/boards/components/issue_card_inner.vue';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { listObj, boardsMockInterceptor, setMockEndpoints } from './mock_data';
+import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
-describe('Board card', () => {
+describe('BoardCard', () => {
let wrapper;
let mock;
let list;
@@ -30,7 +30,7 @@ describe('Board card', () => {
// this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
const mountComponent = propsData => {
- wrapper = shallowMount(boardCard, {
+ wrapper = mount(BoardCard, {
stubs: {
issueCardInner,
},
@@ -38,16 +38,18 @@ describe('Board card', () => {
propsData: {
list,
issue: list.issues[0],
- issueLinkBase: '/',
disabled: false,
index: 0,
- rootPath: '/',
...propsData,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
});
};
- const setupData = () => {
+ const setupData = async () => {
list = new List(listObj);
boardsStore.create();
boardsStore.detail.issue = {};
@@ -58,9 +60,9 @@ describe('Board card', () => {
text_color: 'white',
description: 'test',
});
- return waitForPromises().then(() => {
- list.issues[0].labels.push(label1);
- });
+ await waitForPromises();
+
+ list.issues[0].labels.push(label1);
};
beforeEach(() => {
@@ -79,7 +81,7 @@ describe('Board card', () => {
it('when details issue is empty does not show the element', () => {
mountComponent();
- expect(wrapper.classes()).not.toContain('is-active');
+ expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active');
});
it('when detailIssue is equal to card issue shows the element', () => {
@@ -124,29 +126,6 @@ describe('Board card', () => {
});
describe('mouse events', () => {
- it('sets showDetail to true on mousedown', () => {
- mountComponent();
- wrapper.trigger('mousedown');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.showDetail).toBe(true);
- });
- });
-
- it('sets showDetail to false on mousemove', () => {
- mountComponent();
- wrapper.trigger('mousedown');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.vm.showDetail).toBe(true);
- wrapper.trigger('mousemove');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.vm.showDetail).toBe(false);
- });
- });
-
it('does not set detail issue if showDetail is false', () => {
mountComponent();
expect(boardsStore.detail.issue).toEqual({});
@@ -219,6 +198,9 @@ describe('Board card', () => {
boardsStore.detail.issue = {};
mountComponent();
+ // sets conditional so that event is emitted.
+ wrapper.trigger('mousedown');
+
wrapper.trigger('mouseup');
expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll');
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index c06b7aceaad..2a4dbbb989e 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -59,10 +59,11 @@ describe('Board Column Component', () => {
propsData: {
boardId,
disabled: false,
- issueLinkBase: '/',
- rootPath: '/',
list,
},
+ provide: {
+ boardId,
+ },
});
};
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
new file mode 100644
index 00000000000..df117d06cdf
--- /dev/null
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -0,0 +1,64 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
+import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
+import getters from 'ee_else_ce/boards/stores/getters';
+import { mockListsWithModel } from '../mock_data';
+import BoardContent from '~/boards/components/board_content.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('BoardContent', () => {
+ let wrapper;
+
+ const defaultState = {
+ isShowingEpicsSwimlanes: false,
+ boardLists: mockListsWithModel,
+ error: undefined,
+ };
+
+ const createStore = (state = defaultState) => {
+ return new Vuex.Store({
+ getters,
+ state,
+ actions: {
+ fetchIssuesForAllLists: () => {},
+ },
+ });
+ };
+
+ const createComponent = state => {
+ const store = createStore({
+ ...defaultState,
+ ...state,
+ });
+ wrapper = shallowMount(BoardContent, {
+ localVue,
+ propsData: {
+ lists: mockListsWithModel,
+ canAdminList: true,
+ disabled: false,
+ },
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a BoardColumn component per list', () => {
+ expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length);
+ });
+
+ it('does not display EpicsSwimlanes component', () => {
+ expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index b1d277863e8..65d8070192c 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -11,7 +11,7 @@ describe('board_form.vue', () => {
const propsData = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
- milestonePath: `${TEST_HOST}/milestone/path`,
+ labelsWebUrl: `${TEST_HOST}/-/labels`,
};
const findModal = () => wrapper.find(DeprecatedModal);
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 76a3d5e71c8..2439c347bf0 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -57,12 +57,12 @@ describe('Board List Header Component', () => {
wrapper = shallowMount(BoardListHeader, {
propsData: {
- boardId,
disabled: false,
- issueLinkBase: '/',
- rootPath: '/',
list,
},
+ provide: {
+ boardId,
+ },
});
};
@@ -106,7 +106,7 @@ describe('Board List Header Component', () => {
createComponent();
expect(isCollapsed()).toBe(false);
- wrapper.find('[data-testid="board-list-header"]').vm.$emit('click');
+ wrapper.find('[data-testid="board-list-header"]').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(isCollapsed()).toBe(false);
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index f39adc0fc49..12c9431f2d4 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -6,8 +6,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDrawer, GlLabel } from '@gitlab/ui';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import boardsStore from '~/boards/stores/boards_store';
+import { createStore } from '~/boards/stores';
import sidebarEventHub from '~/sidebar/event_hub';
-import { inactiveId } from '~/boards/constants';
+import { inactiveId, LIST } from '~/boards/constants';
const localVue = createLocalVue();
@@ -16,19 +17,12 @@ localVue.use(Vuex);
describe('BoardSettingsSidebar', () => {
let wrapper;
let mock;
- let storeActions;
+ let store;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
- const createComponent = (state = { activeId: inactiveId }, actions = {}) => {
- storeActions = actions;
-
- const store = new Vuex.Store({
- state,
- actions: storeActions,
- });
-
+ const createComponent = () => {
wrapper = shallowMount(BoardSettingsSidebar, {
store,
localVue,
@@ -38,6 +32,9 @@ describe('BoardSettingsSidebar', () => {
const findDrawer = () => wrapper.find(GlDrawer);
beforeEach(() => {
+ store = createStore();
+ store.state.activeId = inactiveId;
+ store.state.sidebarType = LIST;
boardsStore.create();
});
@@ -46,114 +43,125 @@ describe('BoardSettingsSidebar', () => {
wrapper.destroy();
});
- it('finds a GlDrawer component', () => {
- createComponent();
+ describe('when sidebarType is "list"', () => {
+ it('finds a GlDrawer component', () => {
+ createComponent();
- expect(findDrawer().exists()).toBe(true);
- });
+ expect(findDrawer().exists()).toBe(true);
+ });
- describe('on close', () => {
- it('calls closeSidebar', async () => {
- const spy = jest.fn();
- createComponent({ activeId: inactiveId }, { setActiveId: spy });
+ describe('on close', () => {
+ it('closes the sidebar', async () => {
+ createComponent();
- findDrawer().vm.$emit('close');
+ findDrawer().vm.$emit('close');
- await wrapper.vm.$nextTick();
+ await wrapper.vm.$nextTick();
- expect(storeActions.setActiveId).toHaveBeenCalledWith(
- expect.anything(),
- inactiveId,
- undefined,
- );
- });
+ expect(wrapper.find(GlDrawer).exists()).toBe(false);
+ });
- it('calls closeSidebar on sidebar.closeAll event', async () => {
- createComponent({ activeId: inactiveId }, { setActiveId: jest.fn() });
+ it('closes the sidebar when emitting the correct event', async () => {
+ createComponent();
- sidebarEventHub.$emit('sidebar.closeAll');
+ sidebarEventHub.$emit('sidebar.closeAll');
- await wrapper.vm.$nextTick();
+ await wrapper.vm.$nextTick();
- expect(storeActions.setActiveId).toHaveBeenCalledWith(
- expect.anything(),
- inactiveId,
- undefined,
- );
+ expect(wrapper.find(GlDrawer).exists()).toBe(false);
+ });
});
- });
- describe('when activeId is zero', () => {
- it('renders GlDrawer with open false', () => {
- createComponent();
+ describe('when activeId is zero', () => {
+ it('renders GlDrawer with open false', () => {
+ createComponent();
- expect(findDrawer().props('open')).toBe(false);
+ expect(findDrawer().props('open')).toBe(false);
+ });
});
- });
- describe('when activeId is greater than zero', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
+ describe('when activeId is greater than zero', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ boardsStore.addList({
+ id: listId,
+ label: { title: labelTitle, color: labelColor },
+ list_type: 'label',
+ });
+ store.state.activeId = 1;
+ store.state.sidebarType = LIST;
+ });
- boardsStore.addList({
- id: listId,
- label: { title: labelTitle, color: labelColor },
- list_type: 'label',
+ afterEach(() => {
+ boardsStore.removeList(listId);
});
- });
- afterEach(() => {
- boardsStore.removeList(listId);
+ it('renders GlDrawer with open false', () => {
+ createComponent();
+
+ expect(findDrawer().props('open')).toBe(true);
+ });
});
- it('renders GlDrawer with open false', () => {
- createComponent({ activeId: 1 });
+ describe('when activeId is in boardsStore', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
- expect(findDrawer().props('open')).toBe(true);
- });
- });
+ boardsStore.addList({
+ id: listId,
+ label: { title: labelTitle, color: labelColor },
+ list_type: 'label',
+ });
- describe('when activeId is in boardsStore', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
+ store.state.activeId = listId;
+ store.state.sidebarType = LIST;
- boardsStore.addList({
- id: listId,
- label: { title: labelTitle, color: labelColor },
- list_type: 'label',
+ createComponent();
});
- createComponent({ activeId: listId });
- });
+ afterEach(() => {
+ mock.restore();
+ });
- afterEach(() => {
- mock.restore();
- });
+ it('renders label title', () => {
+ expect(findLabel().props('title')).toBe(labelTitle);
+ });
- it('renders label title', () => {
- expect(findLabel().props('title')).toBe(labelTitle);
+ it('renders label background color', () => {
+ expect(findLabel().props('backgroundColor')).toBe(labelColor);
+ });
});
- it('renders label background color', () => {
- expect(findLabel().props('backgroundColor')).toBe(labelColor);
- });
- });
+ describe('when activeId is not in boardsStore', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
- describe('when activeId is not in boardsStore', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
+ boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
+
+ store.state.activeId = inactiveId;
- boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
+ createComponent();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
- createComponent({ activeId: inactiveId });
+ it('does not render GlLabel', () => {
+ expect(findLabel().exists()).toBe(false);
+ });
});
+ });
- afterEach(() => {
- mock.restore();
+ describe('when sidebarType is not List', () => {
+ beforeEach(() => {
+ store.state.sidebarType = '';
+ createComponent();
});
- it('does not render GlLabel', () => {
- expect(findLabel().exists()).toBe(false);
+ it('does not render GlDrawer', () => {
+ expect(findDrawer().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index f2d4de238d1..2b7605a3f7c 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -81,12 +81,12 @@ describe('BoardsSelector', () => {
assignee_id: null,
labels: [],
},
- milestonePath: `${TEST_HOST}/milestone/path`,
boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false,
canAdminBoard: true,
multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
+ labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true,
diff --git a/spec/frontend/boards/components/issuable_title_spec.js b/spec/frontend/boards/components/issuable_title_spec.js
new file mode 100644
index 00000000000..4b7f491b998
--- /dev/null
+++ b/spec/frontend/boards/components/issuable_title_spec.js
@@ -0,0 +1,33 @@
+import { shallowMount } from '@vue/test-utils';
+import IssuableTitle from '~/boards/components/issuable_title.vue';
+
+describe('IssuableTitle', () => {
+ let wrapper;
+ const defaultProps = {
+ title: 'One',
+ refPath: 'path',
+ };
+ const createComponent = () => {
+ wrapper = shallowMount(IssuableTitle, {
+ propsData: { ...defaultProps },
+ });
+ };
+ const findIssueContent = () => wrapper.find('[data-testid="issue-title"]');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders a title of an issue in the sidebar', () => {
+ expect(findIssueContent().text()).toContain('One');
+ });
+
+ it('renders a referencePath of an issue in the sidebar', () => {
+ expect(findIssueContent().text()).toContain('path');
+ });
+});
diff --git a/spec/frontend/boards/components/issue_count_spec.js b/spec/frontend/boards/components/issue_count_spec.js
index 819d878f4e2..d1ff0bdbf88 100644
--- a/spec/frontend/boards/components/issue_count_spec.js
+++ b/spec/frontend/boards/components/issue_count_spec.js
@@ -29,7 +29,7 @@ describe('IssueCount', () => {
});
it('does not contains maxIssueCount in the template', () => {
- expect(vm.contains('.js-max-issue-size')).toBe(false);
+ expect(vm.find('.js-max-issue-size').exists()).toBe(false);
});
});
diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
new file mode 100644
index 00000000000..1dbcbd06407
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
@@ -0,0 +1,107 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vue';
+
+describe('boards sidebar remove issue', () => {
+ let wrapper;
+
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findEditButton = () => wrapper.find('[data-testid="edit-button"]');
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+ const findExpanded = () => wrapper.find('[data-testid="expanded-content"]');
+
+ const createComponent = ({ props = {}, slots = {}, canUpdate = false } = {}) => {
+ wrapper = shallowMount(BoardSidebarItem, {
+ attachTo: document.body,
+ provide: { canUpdate },
+ propsData: props,
+ slots,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('template', () => {
+ it('renders title', () => {
+ const title = 'Sidebar item title';
+ createComponent({ props: { title } });
+
+ expect(findTitle().text()).toBe(title);
+ });
+
+ it('hides edit button, loader and expanded content by default', () => {
+ createComponent();
+
+ expect(findEditButton().exists()).toBe(false);
+ expect(findLoader().exists()).toBe(false);
+ expect(findExpanded().isVisible()).toBe(false);
+ });
+
+ it('shows "None" if empty collapsed slot', () => {
+ createComponent({});
+
+ expect(findCollapsed().text()).toBe('None');
+ });
+
+ it('renders collapsed content by default', () => {
+ const slots = { collapsed: '<div>Collapsed content</div>' };
+ createComponent({ slots });
+
+ expect(findCollapsed().text()).toBe('Collapsed content');
+ });
+
+ it('shows edit button if can update', () => {
+ createComponent({ canUpdate: true });
+
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('shows loading icon if loading', () => {
+ createComponent({ props: { loading: true } });
+
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('shows expanded content and hides collapsed content when clicking edit button', async () => {
+ const slots = { default: '<div>Select item</div>' };
+ createComponent({ canUpdate: true, slots });
+ findEditButton().vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCollapsed().isVisible()).toBe(false);
+ expect(findExpanded().isVisible()).toBe(true);
+ expect(findExpanded().text()).toBe('Select item');
+ });
+ });
+ });
+
+ describe('collapsing an item by offclicking', () => {
+ beforeEach(async () => {
+ createComponent({ canUpdate: true });
+ findEditButton().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('hides expanded section and displays collapsed section', async () => {
+ expect(findExpanded().isVisible()).toBe(true);
+ document.body.click();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findExpanded().isVisible()).toBe(false);
+ });
+
+ it('emits changed event', async () => {
+ document.body.click();
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted().changed[1][0]).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/boards/issue_card_spec.js b/spec/frontend/boards/issue_card_spec.js
index dee8cb7b6e5..7e22e9647f0 100644
--- a/spec/frontend/boards/issue_card_spec.js
+++ b/spec/frontend/boards/issue_card_spec.js
@@ -47,13 +47,15 @@ describe('Issue card component', () => {
propsData: {
list,
issue,
- issueLinkBase: '/test',
- rootPath: '/',
},
store,
stubs: {
GlLabel: true,
},
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ },
});
});
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
index b731bb6e474..9c3a6e66ef4 100644
--- a/spec/frontend/boards/list_spec.js
+++ b/spec/frontend/boards/list_spec.js
@@ -184,6 +184,7 @@ describe('List model', () => {
}),
);
list.issues = [];
+ global.gon.features = { boardsWithSwimlanes: false };
});
it('adds new issue to top of list', done => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 8ef6efe23c7..5776332c499 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,3 +1,9 @@
+/* global ListIssue */
+/* global List */
+
+import Vue from 'vue';
+import '~/boards/models/list';
+import '~/boards/models/issue';
import boardsStore from '~/boards/stores/boards_store';
export const boardObj = {
@@ -92,11 +98,64 @@ export const mockMilestone = {
due_date: '2019-12-31',
};
+const assignees = [
+ {
+ id: 'gid://gitlab/User/2',
+ username: 'angelina.herman',
+ name: 'Bernardina Bosco',
+ avatar: 'https://www.gravatar.com/avatar/eb7b664b13a30ad9f9ba4b61d7075470?s=80&d=identicon',
+ webUrl: 'http://127.0.0.1:3000/angelina.herman',
+ },
+];
+
+const labels = [
+ {
+ id: 'gid://gitlab/GroupLabel/5',
+ title: 'Cosync',
+ color: '#34ebec',
+ description: null,
+ },
+];
+
+export const rawIssue = {
+ title: 'Issue 1',
+ id: 'gid://gitlab/Issue/436',
+ iid: 27,
+ dueDate: null,
+ timeEstimate: 0,
+ weight: null,
+ confidential: false,
+ referencePath: 'gitlab-org/test-subgroup/gitlab-test#27',
+ path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/27',
+ labels: {
+ nodes: [
+ {
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing',
+ },
+ ],
+ },
+ assignees: {
+ nodes: assignees,
+ },
+ epic: {
+ id: 'gid://gitlab/Epic/41',
+ },
+};
+
export const mockIssue = {
- title: 'Testing',
- id: 1,
- iid: 1,
+ id: 'gid://gitlab/Issue/436',
+ iid: 27,
+ title: 'Issue 1',
+ dueDate: null,
+ timeEstimate: 0,
+ weight: null,
confidential: false,
+ referencePath: 'gitlab-org/test-subgroup/gitlab-test#27',
+ path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/27',
+ assignees,
labels: [
{
id: 1,
@@ -105,16 +164,64 @@ export const mockIssue = {
description: 'testing',
},
],
- assignees: [
- {
- id: 1,
- name: 'name',
- username: 'username',
- avatar_url: 'http://avatar_url',
- },
- ],
+ epic: {
+ id: 'gid://gitlab/Epic/41',
+ },
};
+export const mockIssueWithModel = new ListIssue(mockIssue);
+
+export const mockIssue2 = {
+ id: 'gid://gitlab/Issue/437',
+ iid: 28,
+ title: 'Issue 2',
+ dueDate: null,
+ timeEstimate: 0,
+ weight: null,
+ confidential: false,
+ referencePath: 'gitlab-org/test-subgroup/gitlab-test#28',
+ path: '/gitlab-org/test-subgroup/gitlab-test/-/issues/28',
+ assignees,
+ labels,
+ epic: {
+ id: 'gid://gitlab/Epic/40',
+ },
+};
+
+export const mockIssue2WithModel = new ListIssue(mockIssue2);
+
+export const mockIssue3 = {
+ id: 'gid://gitlab/Issue/438',
+ iid: 29,
+ title: 'Issue 3',
+ referencePath: '#29',
+ dueDate: null,
+ timeEstimate: 0,
+ weight: null,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/28',
+ assignees,
+ labels,
+ epic: null,
+};
+
+export const mockIssue4 = {
+ id: 'gid://gitlab/Issue/439',
+ iid: 30,
+ title: 'Issue 4',
+ referencePath: '#30',
+ dueDate: null,
+ timeEstimate: 0,
+ weight: null,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/28',
+ assignees,
+ labels,
+ epic: null,
+};
+
+export const mockIssues = [mockIssue, mockIssue2];
+
export const BoardsMockData = {
GET: {
'/test/-/boards/1/lists/300/issues?id=300&page=1': {
@@ -165,3 +272,50 @@ export const setMockEndpoints = (opts = {}) => {
boardId,
});
};
+
+export const mockLists = [
+ {
+ id: 'gid://gitlab/List/1',
+ title: 'Backlog',
+ position: null,
+ listType: 'backlog',
+ collapsed: false,
+ label: null,
+ assignee: null,
+ milestone: null,
+ loading: false,
+ },
+ {
+ id: 'gid://gitlab/List/2',
+ title: 'To Do',
+ position: 0,
+ listType: 'label',
+ collapsed: false,
+ label: {
+ id: 'gid://gitlab/GroupLabel/121',
+ title: 'To Do',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+ },
+ assignee: null,
+ milestone: null,
+ loading: false,
+ },
+];
+
+export const mockListsWithModel = mockLists.map(listMock =>
+ Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
+);
+
+export const mockIssuesByListId = {
+ 'gid://gitlab/List/1': [mockIssue.id, mockIssue3.id, mockIssue4.id],
+ 'gid://gitlab/List/2': mockIssues.map(({ id }) => id),
+};
+
+export const issues = {
+ [mockIssue.id]: mockIssue,
+ [mockIssue2.id]: mockIssue2,
+ [mockIssue3.id]: mockIssue3,
+ [mockIssue4.id]: mockIssue4,
+};
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index d539cba76ca..bdbcd435708 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,7 +1,17 @@
import testAction from 'helpers/vuex_action_helper';
-import actions from '~/boards/stores/actions';
+import {
+ mockListsWithModel,
+ mockLists,
+ mockIssue,
+ mockIssueWithModel,
+ mockIssue2WithModel,
+ rawIssue,
+} from '../mock_data';
+import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
-import { inactiveId } from '~/boards/constants';
+import { inactiveId, ListType } from '~/boards/constants';
+import issueMoveListMutation from '~/boards/queries/issue_move_list.mutation.graphql';
+import { fullBoardId } from '~/boards/boards_util';
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -9,6 +19,10 @@ const expectNotImplemented = action => {
});
};
+// We need this helper to make sure projectPath is including
+// subgroups when the movIssue action is called.
+const getProjectPath = path => path.split('#')[0];
+
describe('setInitialBoardData', () => {
it('sets data object', () => {
const mockData = {
@@ -26,6 +40,25 @@ describe('setInitialBoardData', () => {
});
});
+describe('setFilters', () => {
+ it('should commit mutation SET_FILTERS', done => {
+ const state = {
+ filters: {},
+ };
+
+ const filters = { labelName: 'label' };
+
+ testAction(
+ actions.setFilters,
+ filters,
+ state,
+ [{ type: types.SET_FILTERS, payload: filters }],
+ [],
+ done,
+ );
+ });
+});
+
describe('setActiveId', () => {
it('should commit mutation SET_ACTIVE_ID', done => {
const state = {
@@ -34,17 +67,40 @@ describe('setActiveId', () => {
testAction(
actions.setActiveId,
- 1,
+ { id: 1, sidebarType: 'something' },
state,
- [{ type: types.SET_ACTIVE_ID, payload: 1 }],
+ [{ type: types.SET_ACTIVE_ID, payload: { id: 1, sidebarType: 'something' } }],
[],
done,
);
});
});
-describe('fetchLists', () => {
- expectNotImplemented(actions.fetchLists);
+describe('showWelcomeList', () => {
+ it('should dispatch addList action', done => {
+ const state = {
+ endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ boardType: 'group',
+ disabled: false,
+ boardLists: [{ type: 'backlog' }, { type: 'closed' }],
+ };
+
+ const blankList = {
+ id: 'blank',
+ listType: ListType.blank,
+ title: 'Welcome to your issue board!',
+ position: 0,
+ };
+
+ testAction(
+ actions.showWelcomeList,
+ {},
+ state,
+ [],
+ [{ type: 'addList', payload: blankList }],
+ done,
+ );
+ });
});
describe('generateDefaultLists', () => {
@@ -52,29 +108,316 @@ describe('generateDefaultLists', () => {
});
describe('createList', () => {
- expectNotImplemented(actions.createList);
+ it('should dispatch addList action when creating backlog list', done => {
+ const backlogList = {
+ id: 'gid://gitlab/List/1',
+ listType: 'backlog',
+ title: 'Open',
+ position: 0,
+ };
+
+ jest.spyOn(gqlClient, 'mutate').mockReturnValue(
+ Promise.resolve({
+ data: {
+ boardListCreate: {
+ list: backlogList,
+ errors: [],
+ },
+ },
+ }),
+ );
+
+ const state = {
+ endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ boardType: 'group',
+ disabled: false,
+ boardLists: [{ type: 'closed' }],
+ };
+
+ testAction(
+ actions.createList,
+ { backlog: true },
+ state,
+ [],
+ [{ type: 'addList', payload: backlogList }],
+ done,
+ );
+ });
+
+ it('should commit CREATE_LIST_FAILURE mutation when API returns an error', done => {
+ jest.spyOn(gqlClient, 'mutate').mockReturnValue(
+ Promise.resolve({
+ data: {
+ boardListCreate: {
+ list: {},
+ errors: [{ foo: 'bar' }],
+ },
+ },
+ }),
+ );
+
+ const state = {
+ endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ boardType: 'group',
+ disabled: false,
+ boardLists: [{ type: 'closed' }],
+ };
+
+ testAction(
+ actions.createList,
+ { backlog: true },
+ state,
+ [{ type: types.CREATE_LIST_FAILURE }],
+ [],
+ done,
+ );
+ });
+});
+
+describe('moveList', () => {
+ it('should commit MOVE_LIST mutation and dispatch updateList action', done => {
+ const state = {
+ endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ boardType: 'group',
+ disabled: false,
+ boardLists: mockListsWithModel,
+ };
+
+ testAction(
+ actions.moveList,
+ { listId: 'gid://gitlab/List/1', newIndex: 1, adjustmentValue: 1 },
+ state,
+ [
+ {
+ type: types.MOVE_LIST,
+ payload: { movedList: mockListsWithModel[0], listAtNewIndex: mockListsWithModel[1] },
+ },
+ ],
+ [
+ {
+ type: 'updateList',
+ payload: { listId: 'gid://gitlab/List/1', position: 0, backupList: mockListsWithModel },
+ },
+ ],
+ done,
+ );
+ });
});
describe('updateList', () => {
- expectNotImplemented(actions.updateList);
+ it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ updateBoardList: {
+ list: {},
+ errors: [{ foo: 'bar' }],
+ },
+ },
+ });
+
+ const state = {
+ endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ boardType: 'group',
+ disabled: false,
+ boardLists: [{ type: 'closed' }],
+ };
+
+ testAction(
+ actions.updateList,
+ { listId: 'gid://gitlab/List/1', position: 1 },
+ state,
+ [{ type: types.UPDATE_LIST_FAILURE }],
+ [],
+ done,
+ );
+ });
});
describe('deleteList', () => {
expectNotImplemented(actions.deleteList);
});
-describe('fetchIssuesForList', () => {
- expectNotImplemented(actions.fetchIssuesForList);
-});
-
describe('moveIssue', () => {
- expectNotImplemented(actions.moveIssue);
+ const listIssues = {
+ 'gid://gitlab/List/1': [436, 437],
+ 'gid://gitlab/List/2': [],
+ };
+
+ const issues = {
+ '436': mockIssueWithModel,
+ '437': mockIssue2WithModel,
+ };
+
+ const state = {
+ endpoints: { fullPath: 'gitlab-org', boardId: '1' },
+ boardType: 'group',
+ disabled: false,
+ boardLists: mockListsWithModel,
+ issuesByListId: listIssues,
+ issues,
+ };
+
+ it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_SUCCESS mutation when successful', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ issueMoveList: {
+ issue: rawIssue,
+ errors: [],
+ },
+ },
+ });
+
+ testAction(
+ actions.moveIssue,
+ {
+ issueId: '436',
+ issueIid: mockIssue.iid,
+ issuePath: mockIssue.referencePath,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ },
+ state,
+ [
+ {
+ type: types.MOVE_ISSUE,
+ payload: {
+ originalIssue: mockIssueWithModel,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ },
+ },
+ {
+ type: types.MOVE_ISSUE_SUCCESS,
+ payload: { issue: rawIssue },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('calls mutate with the correct variables', () => {
+ const mutationVariables = {
+ mutation: issueMoveListMutation,
+ variables: {
+ projectPath: getProjectPath(mockIssue.referencePath),
+ boardId: fullBoardId(state.endpoints.boardId),
+ iid: mockIssue.iid,
+ fromListId: 1,
+ toListId: 2,
+ moveBeforeId: undefined,
+ moveAfterId: undefined,
+ },
+ };
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ issueMoveList: {
+ issue: rawIssue,
+ errors: [],
+ },
+ },
+ });
+
+ actions.moveIssue(
+ { state, commit: () => {} },
+ {
+ issueId: mockIssue.id,
+ issueIid: mockIssue.iid,
+ issuePath: mockIssue.referencePath,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ },
+ );
+
+ expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
+ });
+
+ it('should commit MOVE_ISSUE mutation and MOVE_ISSUE_FAILURE mutation when unsuccessful', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ issueMoveList: {
+ issue: {},
+ errors: [{ foo: 'bar' }],
+ },
+ },
+ });
+
+ testAction(
+ actions.moveIssue,
+ {
+ issueId: '436',
+ issueIid: mockIssue.iid,
+ issuePath: mockIssue.referencePath,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ },
+ state,
+ [
+ {
+ type: types.MOVE_ISSUE,
+ payload: {
+ originalIssue: mockIssueWithModel,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ },
+ },
+ {
+ type: types.MOVE_ISSUE_FAILURE,
+ payload: {
+ originalIssue: mockIssueWithModel,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ originalIndex: 0,
+ },
+ },
+ ],
+ [],
+ done,
+ );
+ });
});
describe('createNewIssue', () => {
expectNotImplemented(actions.createNewIssue);
});
+describe('addListIssue', () => {
+ it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
+ const payload = {
+ list: mockLists[0],
+ issue: mockIssue,
+ position: 0,
+ };
+
+ testAction(
+ actions.addListIssue,
+ payload,
+ {},
+ [{ type: types.ADD_ISSUE_TO_LIST, payload }],
+ [],
+ done,
+ );
+ });
+});
+
+describe('addListIssueFailure', () => {
+ it('should commit UPDATE_LIST_FAILURE mutation when API returns an error', done => {
+ const payload = {
+ list: mockLists[0],
+ issue: mockIssue,
+ };
+
+ testAction(
+ actions.addListIssueFailure,
+ payload,
+ {},
+ [{ type: types.ADD_ISSUE_TO_LIST_FAILURE, payload }],
+ [],
+ done,
+ );
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 38b2333e679..288143a0f21 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -1,4 +1,6 @@
import getters from '~/boards/stores/getters';
+import { inactiveId } from '~/boards/constants';
+import { mockIssue, mockIssue2, mockIssues, mockIssuesByListId, issues } from '../mock_data';
describe('Boards - Getters', () => {
describe('getLabelToggleState', () => {
@@ -18,4 +20,114 @@ describe('Boards - Getters', () => {
expect(getters.getLabelToggleState(state)).toBe('off');
});
});
+
+ describe('isSidebarOpen', () => {
+ it('returns true when activeId is not equal to 0', () => {
+ const state = {
+ activeId: 1,
+ };
+
+ expect(getters.isSidebarOpen(state)).toBe(true);
+ });
+
+ it('returns false when activeId is equal to 0', () => {
+ const state = {
+ activeId: inactiveId,
+ };
+
+ expect(getters.isSidebarOpen(state)).toBe(false);
+ });
+ });
+
+ describe('isSwimlanesOn', () => {
+ afterEach(() => {
+ window.gon = { features: {} };
+ });
+
+ describe('when boardsWithSwimlanes is true', () => {
+ beforeEach(() => {
+ window.gon = { features: { boardsWithSwimlanes: true } };
+ });
+
+ describe('when isShowingEpicsSwimlanes is true', () => {
+ it('returns true', () => {
+ const state = {
+ isShowingEpicsSwimlanes: true,
+ };
+
+ expect(getters.isSwimlanesOn(state)).toBe(true);
+ });
+ });
+
+ describe('when isShowingEpicsSwimlanes is false', () => {
+ it('returns false', () => {
+ const state = {
+ isShowingEpicsSwimlanes: false,
+ };
+
+ expect(getters.isSwimlanesOn(state)).toBe(false);
+ });
+ });
+ });
+
+ describe('when boardsWithSwimlanes is false', () => {
+ describe('when isShowingEpicsSwimlanes is true', () => {
+ it('returns false', () => {
+ const state = {
+ isShowingEpicsSwimlanes: true,
+ };
+
+ expect(getters.isSwimlanesOn(state)).toBe(false);
+ });
+ });
+
+ describe('when isShowingEpicsSwimlanes is false', () => {
+ it('returns false', () => {
+ const state = {
+ isShowingEpicsSwimlanes: false,
+ };
+
+ expect(getters.isSwimlanesOn(state)).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('getIssueById', () => {
+ const state = { issues: { '1': 'issue' } };
+
+ it.each`
+ id | expected
+ ${'1'} | ${'issue'}
+ ${''} | ${{}}
+ `('returns $expected when $id is passed to state', ({ id, expected }) => {
+ expect(getters.getIssueById(state)(id)).toEqual(expected);
+ });
+ });
+
+ describe('getActiveIssue', () => {
+ it.each`
+ id | expected
+ ${'1'} | ${'issue'}
+ ${''} | ${{}}
+ `('returns $expected when $id is passed to state', ({ id, expected }) => {
+ const state = { issues: { '1': 'issue' }, activeId: id };
+
+ expect(getters.getActiveIssue(state)).toEqual(expected);
+ });
+ });
+
+ describe('getIssues', () => {
+ const boardsState = {
+ issuesByListId: mockIssuesByListId,
+ issues,
+ };
+ it('returns issues for a given listId', () => {
+ const getIssueById = issueId => [mockIssue, mockIssue2].find(({ id }) => id === issueId);
+
+ expect(getters.getIssues(boardsState, { getIssueById })('gid://gitlab/List/2')).toEqual(
+ mockIssues,
+ );
+ });
+ });
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index c1f7f3dda6e..a13a99a507e 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,6 +1,17 @@
import mutations from '~/boards/stores/mutations';
+import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
-import { mockIssue } from '../mock_data';
+import {
+ listObj,
+ listObjDuplicate,
+ mockListsWithModel,
+ mockLists,
+ rawIssue,
+ mockIssue,
+ mockIssue2,
+ mockIssueWithModel,
+ mockIssue2WithModel,
+} from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -26,21 +37,56 @@ describe('Board Store Mutations', () => {
fullPath: 'gitlab-org',
};
const boardType = 'group';
+ const disabled = false;
+ const showPromotion = false;
- mutations.SET_INITIAL_BOARD_DATA(state, { ...endpoints, boardType });
+ mutations[types.SET_INITIAL_BOARD_DATA](state, {
+ ...endpoints,
+ boardType,
+ disabled,
+ showPromotion,
+ });
expect(state.endpoints).toEqual(endpoints);
expect(state.boardType).toEqual(boardType);
+ expect(state.disabled).toEqual(disabled);
+ expect(state.showPromotion).toEqual(showPromotion);
+ });
+ });
+
+ describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
+ it('Should set boardLists to state', () => {
+ const lists = [listObj, listObjDuplicate];
+
+ mutations[types.RECEIVE_BOARD_LISTS_SUCCESS](state, lists);
+
+ expect(state.boardLists).toEqual(lists);
});
});
describe('SET_ACTIVE_ID', () => {
- it('updates activeListId to be the value that is passed', () => {
- const expectedId = 1;
+ const expected = { id: 1, sidebarType: '' };
- mutations.SET_ACTIVE_ID(state, expectedId);
+ beforeEach(() => {
+ mutations.SET_ACTIVE_ID(state, expected);
+ });
+
+ it('updates aciveListId to be the value that is passed', () => {
+ expect(state.activeId).toBe(expected.id);
+ });
- expect(state.activeId).toBe(expectedId);
+ it('updates sidebarType to be the value that is passed', () => {
+ expect(state.sidebarType).toBe(expected.sidebarType);
+ });
+ });
+
+ describe('SET_FILTERS', () => {
+ it('updates filterParams to be the value that is passed', () => {
+ const filterParams = { labelName: 'label' };
+
+ mutations.SET_FILTERS(state, filterParams);
+
+ expect(state.filterParams).toBe(filterParams);
});
});
@@ -56,16 +102,35 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_ADD_LIST_ERROR);
});
- describe('REQUEST_UPDATE_LIST', () => {
- expectNotImplemented(mutations.REQUEST_UPDATE_LIST);
- });
+ describe('MOVE_LIST', () => {
+ it('updates boardLists state with reordered lists', () => {
+ state = {
+ ...state,
+ boardLists: mockListsWithModel,
+ };
- describe('RECEIVE_UPDATE_LIST_SUCCESS', () => {
- expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_SUCCESS);
+ mutations.MOVE_LIST(state, {
+ movedList: mockListsWithModel[0],
+ listAtNewIndex: mockListsWithModel[1],
+ });
+
+ expect(state.boardLists).toEqual([mockListsWithModel[1], mockListsWithModel[0]]);
+ });
});
- describe('RECEIVE_UPDATE_LIST_ERROR', () => {
- expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_ERROR);
+ describe('UPDATE_LIST_FAILURE', () => {
+ it('updates boardLists state with previous order and sets error message', () => {
+ state = {
+ ...state,
+ boardLists: [mockListsWithModel[1], mockListsWithModel[0]],
+ error: undefined,
+ };
+
+ mutations.UPDATE_LIST_FAILURE(state, mockListsWithModel);
+
+ expect(state.boardLists).toEqual(mockListsWithModel);
+ expect(state.error).toEqual('An error occurred while updating the list. Please try again.');
+ });
});
describe('REQUEST_REMOVE_LIST', () => {
@@ -80,6 +145,33 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR);
});
+ describe('RECEIVE_ISSUES_FOR_LIST_SUCCESS', () => {
+ it('updates issuesByListId and issues on state', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id],
+ };
+ const issues = {
+ '1': mockIssue,
+ };
+
+ state = {
+ ...state,
+ isLoadingIssues: true,
+ issuesByListId: {},
+ issues: {},
+ boardLists: mockListsWithModel,
+ };
+
+ mutations.RECEIVE_ISSUES_FOR_LIST_SUCCESS(state, {
+ listIssues: { listData: listIssues, issues },
+ listId: 'gid://gitlab/List/1',
+ });
+
+ expect(state.issuesByListId).toEqual(listIssues);
+ expect(state.issues).toEqual(issues);
+ });
+ });
+
describe('REQUEST_ISSUES_FOR_ALL_LISTS', () => {
it('sets isLoadingIssues to true', () => {
expect(state.isLoadingIssues).toBe(false);
@@ -90,22 +182,45 @@ describe('Board Store Mutations', () => {
});
});
+ describe('RECEIVE_ISSUES_FOR_LIST_FAILURE', () => {
+ it('sets error message', () => {
+ state = {
+ ...state,
+ boardLists: mockListsWithModel,
+ error: undefined,
+ };
+
+ const listId = 'gid://gitlab/List/1';
+
+ mutations.RECEIVE_ISSUES_FOR_LIST_FAILURE(state, listId);
+
+ expect(state.error).toEqual(
+ 'An error occurred while fetching the board issues. Please reload the page.',
+ );
+ });
+ });
+
describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => {
it('sets isLoadingIssues to false and updates issuesByListId object', () => {
const listIssues = {
- '1': [mockIssue],
+ 'gid://gitlab/List/1': [mockIssue.id],
+ };
+ const issues = {
+ '1': mockIssue,
};
state = {
...state,
isLoadingIssues: true,
issuesByListId: {},
+ issues: {},
};
- mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS(state, listIssues);
+ mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS(state, { listData: listIssues, issues });
expect(state.isLoadingIssues).toBe(false);
expect(state.issuesByListId).toEqual(listIssues);
+ expect(state.issues).toEqual(issues);
});
});
@@ -113,6 +228,65 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.REQUEST_ADD_ISSUE);
});
+ describe('RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE', () => {
+ it('sets isLoadingIssues to false and sets error message', () => {
+ state = {
+ ...state,
+ isLoadingIssues: true,
+ error: undefined,
+ };
+
+ mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE(state);
+
+ expect(state.isLoadingIssues).toBe(false);
+ expect(state.error).toEqual(
+ 'An error occurred while fetching the board issues. Please reload the page.',
+ );
+ });
+ });
+
+ describe('UPDATE_ISSUE_BY_ID', () => {
+ const issueId = '1';
+ const prop = 'id';
+ const value = '2';
+ const issue = { [issueId]: { id: 1, title: 'Issue' } };
+
+ beforeEach(() => {
+ state = {
+ ...state,
+ isLoadingIssues: true,
+ error: undefined,
+ issues: {
+ ...issue,
+ },
+ };
+ });
+
+ describe('when the issue is in state', () => {
+ it('updates the property of the correct issue', () => {
+ mutations.UPDATE_ISSUE_BY_ID(state, {
+ issueId,
+ prop,
+ value,
+ });
+
+ expect(state.issues[issueId]).toEqual({ ...issue[issueId], id: '2' });
+ });
+ });
+
+ describe('when the issue is not in state', () => {
+ it('throws an error', () => {
+ expect(() => {
+ mutations.UPDATE_ISSUE_BY_ID(state, {
+ issueId: '3',
+ prop,
+ value,
+ });
+ }).toThrow(new Error('No issue found.'));
+ });
+ });
+ });
+
describe('RECEIVE_ADD_ISSUE_SUCCESS', () => {
expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS);
});
@@ -121,16 +295,86 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR);
});
- describe('REQUEST_MOVE_ISSUE', () => {
- expectNotImplemented(mutations.REQUEST_MOVE_ISSUE);
+ describe('MOVE_ISSUE', () => {
+ it('updates issuesByListId, moving issue between lists', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
+ 'gid://gitlab/List/2': [],
+ };
+
+ const issues = {
+ '1': mockIssueWithModel,
+ '2': mockIssue2WithModel,
+ };
+
+ state = {
+ ...state,
+ issuesByListId: listIssues,
+ boardLists: mockListsWithModel,
+ issues,
+ };
+
+ mutations.MOVE_ISSUE(state, {
+ originalIssue: mockIssue2WithModel,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ });
+
+ const updatedListIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id],
+ 'gid://gitlab/List/2': [mockIssue2.id],
+ };
+
+ expect(state.issuesByListId).toEqual(updatedListIssues);
+ });
});
- describe('RECEIVE_MOVE_ISSUE_SUCCESS', () => {
- expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_SUCCESS);
+ describe('MOVE_ISSUE_SUCCESS', () => {
+ it('updates issue in issues state', () => {
+ const issues = {
+ '436': { id: rawIssue.id },
+ };
+
+ state = {
+ ...state,
+ issues,
+ };
+
+ mutations.MOVE_ISSUE_SUCCESS(state, {
+ issue: rawIssue,
+ });
+
+ expect(state.issues).toEqual({ '436': { ...mockIssueWithModel, id: 436 } });
+ });
});
- describe('RECEIVE_MOVE_ISSUE_ERROR', () => {
- expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_ERROR);
+ describe('MOVE_ISSUE_FAILURE', () => {
+ it('updates issuesByListId, reverting moving issue between lists, and sets error message', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id],
+ 'gid://gitlab/List/2': [mockIssue2.id],
+ };
+
+ state = {
+ ...state,
+ issuesByListId: listIssues,
+ };
+
+ mutations.MOVE_ISSUE_FAILURE(state, {
+ originalIssue: mockIssue2,
+ fromListId: 'gid://gitlab/List/1',
+ toListId: 'gid://gitlab/List/2',
+ originalIndex: 1,
+ });
+
+ const updatedListIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
+ 'gid://gitlab/List/2': [],
+ };
+
+ expect(state.issuesByListId).toEqual(updatedListIssues);
+ expect(state.error).toEqual('An error occurred while moving the issue. Please try again.');
+ });
});
describe('REQUEST_UPDATE_ISSUE', () => {
@@ -145,6 +389,50 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR);
});
+ describe('ADD_ISSUE_TO_LIST', () => {
+ it('adds issue to issues state and issue id in list in issuesByListId', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id],
+ };
+ const issues = {
+ '1': mockIssue,
+ };
+
+ state = {
+ ...state,
+ issuesByListId: listIssues,
+ issues,
+ };
+
+ mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 });
+
+ expect(state.issuesByListId['gid://gitlab/List/1']).toContain(mockIssue2.id);
+ expect(state.issues[mockIssue2.id]).toEqual(mockIssue2);
+ });
+ });
+
+ describe('ADD_ISSUE_TO_LIST_FAILURE', () => {
+ it('removes issue id from list in issuesByListId', () => {
+ const listIssues = {
+ 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id],
+ };
+ const issues = {
+ '1': mockIssue,
+ '2': mockIssue2,
+ };
+
+ state = {
+ ...state,
+ issuesByListId: listIssues,
+ issues,
+ };
+
+ mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issue: mockIssue2 });
+
+ expect(state.issuesByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id);
+ });
+ });
+
describe('SET_CURRENT_PAGE', () => {
expectNotImplemented(mutations.SET_CURRENT_PAGE);
});
diff --git a/spec/frontend/branches/ajax_loading_spinner_spec.js b/spec/frontend/branches/ajax_loading_spinner_spec.js
new file mode 100644
index 00000000000..a6404faa445
--- /dev/null
+++ b/spec/frontend/branches/ajax_loading_spinner_spec.js
@@ -0,0 +1,32 @@
+import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
+
+describe('Ajax Loading Spinner', () => {
+ let ajaxLoadingSpinnerElement;
+ let fauxEvent;
+ beforeEach(() => {
+ document.body.innerHTML = `
+ <div>
+ <a class="js-ajax-loading-spinner"
+ data-remote
+ href="http://goesnowhere.nothing/whereami">
+ <i class="fa fa-trash-o"></i>
+ </a></div>`;
+ AjaxLoadingSpinner.init();
+ ajaxLoadingSpinnerElement = document.querySelector('.js-ajax-loading-spinner');
+ fauxEvent = { target: ajaxLoadingSpinnerElement };
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('`ajaxBeforeSend` event handler sets current icon to spinner and disables link', () => {
+ expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).toBeNull();
+ expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(false);
+
+ AjaxLoadingSpinner.ajaxBeforeSend(fauxEvent);
+
+ expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).not.toBeNull();
+ expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(true);
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index 4e35243f484..ab32fb12058 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -49,7 +49,7 @@ describe('Ci variable modal', () => {
});
it('does not render the autocomplete dropdown', () => {
- expect(wrapper.contains(GlFormCombobox)).toBe(false);
+ expect(wrapper.find(GlFormCombobox).exists()).toBe(false);
});
});
diff --git a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
new file mode 100644
index 00000000000..5577176bcc5
--- /dev/null
+++ b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NewCluster renders the cluster component correctly 1`] = `
+"<div>
+ <h4>Enter the details for your Kubernetes cluster</h4>
+ <p>Please enter access information for your Kubernetes cluster. If you need help, you can read our <b-link-stub href=\\"/some/help/path\\" target=\\"_blank\\" event=\\"click\\" routertag=\\"a\\" class=\\"gl-link\\">documentation</b-link-stub> on Kubernetes</p>
+</div>"
+`;
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index b97d4dbf355..0a964426c95 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -48,7 +48,7 @@ describe('Application Row', () => {
describe('Install button', () => {
const button = () => wrapper.find('.js-cluster-application-install-button');
const checkButtonState = (label, loading, disabled) => {
- expect(button().props('label')).toEqual(label);
+ expect(button().text()).toEqual(label);
expect(button().props('loading')).toEqual(loading);
expect(button().props('disabled')).toEqual(disabled);
};
@@ -56,7 +56,7 @@ describe('Application Row', () => {
it('has indeterminate state on page load', () => {
mountComponent({ status: null });
- expect(button().props('label')).toBeUndefined();
+ expect(button().text()).toBe('');
});
it('has install button', () => {
@@ -225,7 +225,7 @@ describe('Application Row', () => {
mountComponent({ updateAvailable: true });
expect(button().exists()).toBe(true);
- expect(button().props('label')).toContain('Update');
+ expect(button().text()).toContain('Update');
});
it('has enabled "Retry update" when update process fails', () => {
@@ -235,14 +235,14 @@ describe('Application Row', () => {
});
expect(button().exists()).toBe(true);
- expect(button().props('label')).toContain('Retry update');
+ expect(button().text()).toContain('Retry update');
});
it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
mountComponent({ status: APPLICATION_STATUS.UPDATING });
expect(button().exists()).toBe(true);
- expect(button().props('label')).toContain('Updating');
+ expect(button().text()).toContain('Updating');
});
it('clicking update button emits event', () => {
@@ -300,11 +300,11 @@ describe('Application Row', () => {
beforeEach(() => mountComponent({ updateAvailable: true }));
it('the modal is not rendered', () => {
- expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
+ expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
it('the correct button is rendered', () => {
- expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
+ expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
});
});
@@ -318,11 +318,13 @@ describe('Application Row', () => {
});
it('displays a modal', () => {
- expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(true);
+ expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
});
it('the correct button is rendered', () => {
- expect(wrapper.contains("[data-qa-selector='update_button_with_confirmation']")).toBe(true);
+ expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
+ true,
+ );
});
it('triggers updateApplication event', () => {
@@ -344,8 +346,10 @@ describe('Application Row', () => {
version: '1.1.2',
});
- expect(wrapper.contains("[data-qa-selector='update_button_with_confirmation']")).toBe(true);
- expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(true);
+ expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
+ true,
+ );
+ expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
});
it('does not need confirmation is version is 3.0.0', () => {
@@ -355,8 +359,8 @@ describe('Application Row', () => {
version: '3.0.0',
});
- expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
- expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
+ expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
+ expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
it('does not need confirmation if version is higher than 3.0.0', () => {
@@ -366,8 +370,8 @@ describe('Application Row', () => {
version: '5.2.1',
});
- expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
- expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
+ expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
+ expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/clusters/components/fluentd_output_settings_spec.js b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
index 0bc4eb73bf9..c263679a45c 100644
--- a/spec/frontend/clusters/components/fluentd_output_settings_spec.js
+++ b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
@@ -168,7 +168,7 @@ describe('FluentdOutputSettings', () => {
});
it('displays a error message', () => {
- expect(wrapper.contains(GlAlert)).toBe(true);
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
index a07258dcc69..11ebe1b5d61 100644
--- a/spec/frontend/clusters/components/knative_domain_editor_spec.js
+++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem, GlButton } from '@gitlab/ui';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { UPDATING } = APPLICATION_STATUS;
@@ -79,7 +78,7 @@ describe('KnativeDomainEditor', () => {
});
it('triggers save event and pass current knative hostname', () => {
- wrapper.find(LoadingButton).vm.$emit('click');
+ wrapper.find(GlButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('save').length).toEqual(1);
});
@@ -166,15 +165,15 @@ describe('KnativeDomainEditor', () => {
});
it('renders loading spinner in save button', () => {
- expect(wrapper.find(LoadingButton).props('loading')).toBe(true);
+ expect(wrapper.find(GlButton).props('loading')).toBe(true);
});
it('renders disabled save button', () => {
- expect(wrapper.find(LoadingButton).props('disabled')).toBe(true);
+ expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
it('renders save button with "Saving" label', () => {
- expect(wrapper.find(LoadingButton).props('label')).toBe('Saving');
+ expect(wrapper.find(GlButton).text()).toBe('Saving');
});
});
});
diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js
new file mode 100644
index 00000000000..bb4898f98ba
--- /dev/null
+++ b/spec/frontend/clusters/components/new_cluster_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import NewCluster from '~/clusters/components/new_cluster.vue';
+import createClusterStore from '~/clusters/stores/new_cluster';
+
+describe('NewCluster', () => {
+ let store;
+ let wrapper;
+
+ const createWrapper = () => {
+ store = createClusterStore({ clusterConnectHelpPath: '/some/help/path' });
+ wrapper = shallowMount(NewCluster, { store, stubs: { GlLink, GlSprintf } });
+ return wrapper.vm.$nextTick();
+ };
+
+ const findDescription = () => wrapper.find(GlSprintf);
+
+ const findLink = () => wrapper.find(GlLink);
+
+ beforeEach(() => {
+ return createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the cluster component correctly', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('renders the correct information text', () => {
+ expect(findDescription().text()).toContain(
+ 'Please enter access information for your Kubernetes cluster.',
+ );
+ });
+
+ it('renders a valid help link set by the backend', () => {
+ expect(findLink().attributes('href')).toBe('/some/help/path');
+ });
+});
diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js
index 9f9397d4d41..387e2188572 100644
--- a/spec/frontend/clusters/components/uninstall_application_button_spec.js
+++ b/spec/frontend/clusters/components/uninstall_application_button_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS;
@@ -19,14 +19,21 @@ describe('UninstallApplicationButton', () => {
});
describe.each`
- status | loading | disabled | label
+ status | loading | disabled | text
${INSTALLED} | ${false} | ${false} | ${'Uninstall'}
${UPDATING} | ${false} | ${true} | ${'Uninstall'}
${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'}
- `('when app status is $status', ({ loading, disabled, status, label }) => {
- it(`renders a loading=${loading}, disabled=${disabled} button with label="${label}"`, () => {
+ `('when app status is $status', ({ loading, disabled, status, text }) => {
+ beforeAll(() => {
createComponent({ status });
- expect(wrapper.find(LoadingButton).props()).toMatchObject({ loading, disabled, label });
+ });
+
+ it(`renders a button with loading=${loading} and disabled=${disabled}`, () => {
+ expect(wrapper.find(GlButton).props()).toMatchObject({ loading, disabled });
+ });
+
+ it(`renders a button with text="${text}"`, () => {
+ expect(wrapper.find(GlButton).text()).toBe(text);
});
});
});
diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
index cff84180f26..436f1e97b04 100644
--- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js
+++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
@@ -28,7 +28,7 @@ describe('ClustersAncestorNotice', () => {
});
it('displays no notice', () => {
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
});
});
@@ -45,7 +45,7 @@ describe('ClustersAncestorNotice', () => {
});
it('displays link', () => {
- expect(wrapper.contains(GlLink)).toBe(true);
+ expect(wrapper.find(GlLink).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index c6a5f66a627..628c35ae839 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -1,6 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
-import { GlLoadingIcon, GlPagination, GlSkeletonLoading, GlTable } from '@gitlab/ui';
+import {
+ GlLoadingIcon,
+ GlPagination,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+ GlTable,
+} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import Clusters from '~/clusters_list/components/clusters.vue';
diff --git a/spec/frontend/collapsed_sidebar_todo_spec.js b/spec/frontend/collapsed_sidebar_todo_spec.js
index 0c74491aa74..b1a304fabcd 100644
--- a/spec/frontend/collapsed_sidebar_todo_spec.js
+++ b/spec/frontend/collapsed_sidebar_todo_spec.js
@@ -47,9 +47,9 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
expect(
document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg use')
- .getAttribute('xlink:href'),
- ).toContain('todo-add');
+ .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg')
+ .getAttribute('data-testid'),
+ ).toBe('todo-add-icon');
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
@@ -72,9 +72,9 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
expect(
document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg.todo-undone use')
- .getAttribute('xlink:href'),
- ).toContain('todo-done');
+ .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg.todo-undone')
+ .getAttribute('data-testid'),
+ ).toBe('todo-done-icon');
done();
});
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index f7b68d96129..271c6356f7e 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -33,9 +33,9 @@ exports[`Confidential merge request project form group component renders empty s
Read more
</span>
- <i
- aria-hidden="true"
- class="fa fa-question-circle"
+ <gl-icon-stub
+ name="question-o"
+ size="16"
/>
</gl-link-stub>
</p>
@@ -76,9 +76,9 @@ exports[`Confidential merge request project form group component renders fork dr
Read more
</span>
- <i
- aria-hidden="true"
- class="fa fa-question-circle"
+ <gl-icon-stub
+ name="question-o"
+ size="16"
/>
</gl-link-stub>
</p>
diff --git a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
index 14f2a527dfb..17abf409717 100644
--- a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
@@ -92,7 +92,7 @@ describe('ClusterFormDropdown', () => {
});
it('displays a checked GlIcon next to the item', () => {
- expect(wrapper.find(GlIcon).is('.invisible')).toBe(false);
+ expect(wrapper.find(GlIcon).classes()).not.toContain('invisible');
expect(wrapper.find(GlIcon).props('name')).toBe('mobile-issue-close');
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
index 34d9ee733c4..d7dd7072f67 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -147,6 +147,7 @@ describe('EksClusterConfigurationForm', () => {
initialState: {
clusterName: 'cluster name',
environmentScope: '*',
+ kubernetesVersion: '1.16',
selectedRegion: 'region',
selectedRole: 'role',
selectedKeyPair: 'key pair',
@@ -400,43 +401,33 @@ describe('EksClusterConfigurationForm', () => {
});
it('dispatches setRegion action', () => {
- expect(actions.setRegion).toHaveBeenCalledWith(expect.anything(), { region }, undefined);
+ expect(actions.setRegion).toHaveBeenCalledWith(expect.anything(), { region });
});
it('fetches available vpcs', () => {
- expect(vpcsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { region }, undefined);
+ expect(vpcsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { region });
});
it('fetches available key pairs', () => {
- expect(keyPairsActions.fetchItems).toHaveBeenCalledWith(
- expect.anything(),
- { region },
- undefined,
- );
+ expect(keyPairsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { region });
});
it('cleans selected vpc', () => {
- expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc: null }, undefined);
+ expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc: null });
});
it('cleans selected key pair', () => {
- expect(actions.setKeyPair).toHaveBeenCalledWith(
- expect.anything(),
- { keyPair: null },
- undefined,
- );
+ expect(actions.setKeyPair).toHaveBeenCalledWith(expect.anything(), { keyPair: null });
});
it('cleans selected subnet', () => {
- expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] }, undefined);
+ expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] });
});
it('cleans selected security group', () => {
- expect(actions.setSecurityGroup).toHaveBeenCalledWith(
- expect.anything(),
- { securityGroup: null },
- undefined,
- );
+ expect(actions.setSecurityGroup).toHaveBeenCalledWith(expect.anything(), {
+ securityGroup: null,
+ });
});
});
@@ -445,11 +436,7 @@ describe('EksClusterConfigurationForm', () => {
findClusterNameInput().vm.$emit('input', clusterName);
- expect(actions.setClusterName).toHaveBeenCalledWith(
- expect.anything(),
- { clusterName },
- undefined,
- );
+ expect(actions.setClusterName).toHaveBeenCalledWith(expect.anything(), { clusterName });
});
it('dispatches setEnvironmentScope when environment scope input changes', () => {
@@ -457,11 +444,9 @@ describe('EksClusterConfigurationForm', () => {
findEnvironmentScopeInput().vm.$emit('input', environmentScope);
- expect(actions.setEnvironmentScope).toHaveBeenCalledWith(
- expect.anything(),
- { environmentScope },
- undefined,
- );
+ expect(actions.setEnvironmentScope).toHaveBeenCalledWith(expect.anything(), {
+ environmentScope,
+ });
});
it('dispatches setKubernetesVersion when kubernetes version dropdown changes', () => {
@@ -469,11 +454,9 @@ describe('EksClusterConfigurationForm', () => {
findKubernetesVersionDropdown().vm.$emit('input', kubernetesVersion);
- expect(actions.setKubernetesVersion).toHaveBeenCalledWith(
- expect.anything(),
- { kubernetesVersion },
- undefined,
- );
+ expect(actions.setKubernetesVersion).toHaveBeenCalledWith(expect.anything(), {
+ kubernetesVersion,
+ });
});
it('dispatches setGitlabManagedCluster when gitlab managed cluster input changes', () => {
@@ -481,11 +464,9 @@ describe('EksClusterConfigurationForm', () => {
findGitlabManagedClusterCheckbox().vm.$emit('input', gitlabManagedCluster);
- expect(actions.setGitlabManagedCluster).toHaveBeenCalledWith(
- expect.anything(),
- { gitlabManagedCluster },
- undefined,
- );
+ expect(actions.setGitlabManagedCluster).toHaveBeenCalledWith(expect.anything(), {
+ gitlabManagedCluster,
+ });
});
describe('when vpc is selected', () => {
@@ -498,35 +479,28 @@ describe('EksClusterConfigurationForm', () => {
});
it('dispatches setVpc action', () => {
- expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined);
+ expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc });
});
it('cleans selected subnet', () => {
- expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] }, undefined);
+ expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] });
});
it('cleans selected security group', () => {
- expect(actions.setSecurityGroup).toHaveBeenCalledWith(
- expect.anything(),
- { securityGroup: null },
- undefined,
- );
+ expect(actions.setSecurityGroup).toHaveBeenCalledWith(expect.anything(), {
+ securityGroup: null,
+ });
});
it('dispatches fetchSubnets action', () => {
- expect(subnetsActions.fetchItems).toHaveBeenCalledWith(
- expect.anything(),
- { vpc, region },
- undefined,
- );
+ expect(subnetsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { vpc, region });
});
it('dispatches fetchSecurityGroups action', () => {
- expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith(
- expect.anything(),
- { vpc, region },
- undefined,
- );
+ expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), {
+ vpc,
+ region,
+ });
});
});
@@ -538,7 +512,7 @@ describe('EksClusterConfigurationForm', () => {
});
it('dispatches setSubnet action', () => {
- expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet }, undefined);
+ expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet });
});
});
@@ -550,7 +524,7 @@ describe('EksClusterConfigurationForm', () => {
});
it('dispatches setRole action', () => {
- expect(actions.setRole).toHaveBeenCalledWith(expect.anything(), { role }, undefined);
+ expect(actions.setRole).toHaveBeenCalledWith(expect.anything(), { role });
});
});
@@ -562,7 +536,7 @@ describe('EksClusterConfigurationForm', () => {
});
it('dispatches setKeyPair action', () => {
- expect(actions.setKeyPair).toHaveBeenCalledWith(expect.anything(), { keyPair }, undefined);
+ expect(actions.setKeyPair).toHaveBeenCalledWith(expect.anything(), { keyPair });
});
});
@@ -574,11 +548,7 @@ describe('EksClusterConfigurationForm', () => {
});
it('dispatches setSecurityGroup action', () => {
- expect(actions.setSecurityGroup).toHaveBeenCalledWith(
- expect.anything(),
- { securityGroup },
- undefined,
- );
+ expect(actions.setSecurityGroup).toHaveBeenCalledWith(expect.anything(), { securityGroup });
});
});
@@ -590,11 +560,7 @@ describe('EksClusterConfigurationForm', () => {
});
it('dispatches setInstanceType action', () => {
- expect(actions.setInstanceType).toHaveBeenCalledWith(
- expect.anything(),
- { instanceType },
- undefined,
- );
+ expect(actions.setInstanceType).toHaveBeenCalledWith(expect.anything(), { instanceType });
});
});
@@ -603,7 +569,7 @@ describe('EksClusterConfigurationForm', () => {
findNodeCountInput().vm.$emit('input', nodeCount);
- expect(actions.setNodeCount).toHaveBeenCalledWith(expect.anything(), { nodeCount }, undefined);
+ expect(actions.setNodeCount).toHaveBeenCalledWith(expect.anything(), { nodeCount });
});
describe('when all cluster configuration fields are set', () => {
diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
index c58638f5c80..d2d6db31d1b 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
@@ -1,9 +1,7 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-
+import { GlButton } from '@gitlab/ui';
import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
-
import eksClusterState from '~/create_cluster/eks_cluster/store/state';
const localVue = createLocalVue();
@@ -46,7 +44,7 @@ describe('ServiceCredentialsForm', () => {
const findExternalIdInput = () => vm.find('#eks-external-id');
const findCopyExternalIdButton = () => vm.find('.js-copy-external-id-button');
const findInvalidCredentials = () => vm.find('.js-invalid-credentials');
- const findSubmitButton = () => vm.find(LoadingButton);
+ const findSubmitButton = () => vm.find(GlButton);
it('displays provided account id', () => {
expect(findAccountIdInput().attributes('value')).toBe(accountId);
@@ -102,7 +100,7 @@ describe('ServiceCredentialsForm', () => {
});
it('displays Authenticating label on submit button', () => {
- expect(findSubmitButton().props('label')).toBe('Authenticating');
+ expect(findSubmitButton().text()).toBe('Authenticating');
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index 882a4a002bd..ed753888790 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -47,7 +47,7 @@ describe('EKS Cluster Store Actions', () => {
beforeEach(() => {
clusterName = 'my cluster';
environmentScope = 'production';
- kubernetesVersion = '11.1';
+ kubernetesVersion = '1.16';
region = 'regions-1';
vpc = 'vpc-1';
subnet = 'subnet-1';
@@ -180,6 +180,7 @@ describe('EKS Cluster Store Actions', () => {
environment_scope: environmentScope,
managed: gitlabManagedCluster,
provider_aws_attributes: {
+ kubernetes_version: kubernetesVersion,
region,
vpc_id: vpc,
subnet_ids: subnet,
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
index 57ef74f0119..c09eaa63d4d 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
@@ -124,11 +124,7 @@ describe('GkeMachineTypeDropdown', () => {
wrapper.find('.dropdown-content button').trigger('click');
return wrapper.vm.$nextTick().then(() => {
- expect(setMachineType).toHaveBeenCalledWith(
- expect.anything(),
- selectedMachineTypeMock,
- undefined,
- );
+ expect(setMachineType).toHaveBeenCalledWith(expect.anything(), selectedMachineTypeMock);
});
});
});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
index 1df583af711..ce24d186511 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
@@ -121,23 +121,19 @@ describe('GkeNetworkDropdown', () => {
});
it('cleans selected subnetwork', () => {
- expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), '', undefined);
+ expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), '');
});
it('dispatches the setNetwork action', () => {
- expect(setNetwork).toHaveBeenCalledWith(expect.anything(), selectedNetwork, undefined);
+ expect(setNetwork).toHaveBeenCalledWith(expect.anything(), selectedNetwork);
});
it('fetches subnetworks for the selected project, region, and network', () => {
- expect(fetchSubnetworks).toHaveBeenCalledWith(
- expect.anything(),
- {
- project: projectId,
- region,
- network: selectedNetwork.selfLink,
- },
- undefined,
- );
+ expect(fetchSubnetworks).toHaveBeenCalledWith(expect.anything(), {
+ project: projectId,
+ region,
+ network: selectedNetwork.selfLink,
+ });
});
});
});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
index 0d429778a44..eb58108bf3c 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
@@ -130,7 +130,6 @@ describe('GkeProjectIdDropdown', () => {
expect(setProject).toHaveBeenCalledWith(
expect.anything(),
gapiProjectsResponseMock.projects[0],
- undefined,
);
});
});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
index a1dc3960fe9..35e43d5b033 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
@@ -107,7 +107,7 @@ describe('GkeSubnetworkDropdown', () => {
wrapper.find(ClusterFormDropdown).vm.$emit('input', selectedSubnetwork);
- expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), selectedSubnetwork, undefined);
+ expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), selectedSubnetwork);
});
});
});
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
index 644cd0b5f27..99cb864ce34 100644
--- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
+++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
@@ -1,6 +1,6 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlDeprecatedDropdownItem, GlNewDropdown } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem, GlDropdown } from '@gitlab/ui';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
import createStore from '~/deploy_freeze/store';
@@ -92,7 +92,7 @@ describe('Deploy freeze timezone dropdown', () => {
});
it('renders selected time zone as dropdown label', () => {
- expect(wrapper.find(GlNewDropdown).vm.text).toBe('Alaska');
+ expect(wrapper.find(GlDropdown).vm.text).toBe('Alaska');
});
});
});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 7d942d969bb..0b1cbd28274 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -55,20 +55,20 @@ describe('Deploy keys key', () => {
it('shows pencil button for editing', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn .ic-pencil')).toExist();
+ expect(wrapper.find('.btn [data-testid="pencil-icon"]')).toExist();
});
it('shows disable button when the project is not deletable', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn .ic-cancel')).toExist();
+ expect(wrapper.find('.btn [data-testid="cancel-icon"]')).toExist();
});
it('shows remove button when the project is deletable', () => {
createComponent({
deployKey: { ...deployKey, destroyed_when_orphaned: true, almost_orphaned: true },
});
- expect(wrapper.find('.btn .ic-remove')).toExist();
+ expect(wrapper.find('.btn [data-testid="remove-icon"]')).toExist();
});
});
@@ -147,7 +147,7 @@ describe('Deploy keys key', () => {
it('shows pencil button for editing', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn .ic-pencil')).toExist();
+ expect(wrapper.find('.btn [data-testid="pencil-icon"]')).toExist();
});
it('shows disable button when key is enabled', () => {
@@ -155,7 +155,7 @@ describe('Deploy keys key', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn .ic-cancel')).toExist();
+ expect(wrapper.find('.btn [data-testid="cancel-icon"]')).toExist();
});
});
});
diff --git a/spec/frontend/gl_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 8bfe7f56e37..e6323859899 100644
--- a/spec/frontend/gl_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import $ from 'jquery';
-import '~/gl_dropdown';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -9,8 +9,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrl'),
}));
-describe('glDropdown', () => {
- preloadFixtures('static/gl_dropdown.html');
+describe('deprecatedJQueryDropdown', () => {
+ preloadFixtures('static/deprecated_jquery_dropdown.html');
const NON_SELECTABLE_CLASSES =
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
@@ -60,14 +60,12 @@ describe('glDropdown', () => {
id: project => project.id,
...extraOpts,
};
- test.dropdownButtonElement = $(
- '#js-project-dropdown',
- test.dropdownContainerElement,
- ).glDropdown(options);
+ test.dropdownButtonElement = $('#js-project-dropdown', test.dropdownContainerElement);
+ initDeprecatedJQueryDropdown(test.dropdownButtonElement, options);
}
beforeEach(() => {
- loadFixtures('static/gl_dropdown.html');
+ loadFixtures('static/deprecated_jquery_dropdown.html');
test.dropdownContainerElement = $('.dropdown.inline');
test.$dropdownMenuElement = $('.dropdown-menu', test.dropdownContainerElement);
test.projectsData = getJSONFixture('static/projects.json');
@@ -248,9 +246,9 @@ describe('glDropdown', () => {
function dropdownWithOptions(options) {
const $dropdownDiv = $('<div />');
- $dropdownDiv.glDropdown(options);
+ initDeprecatedJQueryDropdown($dropdownDiv, options);
- return $dropdownDiv.data('glDropdown');
+ return $dropdownDiv.data('deprecatedJQueryDropdown');
}
function basicDropdown() {
@@ -315,6 +313,42 @@ describe('glDropdown', () => {
expect(li.childNodes.length).toEqual(1);
expect(li.textContent).toEqual(text);
});
+
+ describe('with a trackSuggestionsClickedLabel', () => {
+ it('it includes data-track attributes', () => {
+ const dropdown = dropdownWithOptions({
+ trackSuggestionClickedLabel: 'some_value_for_label',
+ });
+ const item = {
+ id: 'some-element-id',
+ text: 'the link text',
+ url: 'http://example.com',
+ category: 'Suggestion category',
+ };
+ const li = dropdown.renderItem(item, null, 3);
+ const link = li.querySelector('a');
+
+ expect(link).toHaveAttr('data-track-event', 'click_text');
+ expect(link).toHaveAttr('data-track-label', 'some_value_for_label');
+ expect(link).toHaveAttr('data-track-value', '3');
+ expect(link).toHaveAttr('data-track-property', 'suggestion-category');
+ });
+
+ it('it defaults property to no_category when category not provided', () => {
+ const dropdown = dropdownWithOptions({
+ trackSuggestionClickedLabel: 'some_value_for_label',
+ });
+ const item = {
+ id: 'some-element-id',
+ text: 'the link text',
+ url: 'http://example.com',
+ };
+ const li = dropdown.renderItem(item);
+ const link = li.querySelector('a');
+
+ expect(link).toHaveAttr('data-track-property', 'no-category');
+ });
+ });
});
it('should keep selected item after selecting a second time', () => {
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index cd4ef1f0ccd..961f5bdd2ae 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -8,7 +8,7 @@ describe('Batch delete button component', () => {
const findButton = () => wrapper.find(GlButton);
const findModal = () => wrapper.find(GlModal);
- function createComponent(isDeleting = false) {
+ function createComponent({ isDeleting = false } = {}, { slots = {} } = {}) {
wrapper = shallowMount(BatchDeleteButton, {
propsData: {
isDeleting,
@@ -16,6 +16,7 @@ describe('Batch delete button component', () => {
directives: {
GlModalDirective,
},
+ slots,
});
}
@@ -31,7 +32,7 @@ describe('Batch delete button component', () => {
});
it('renders disabled button when design is deleting', () => {
- createComponent(true);
+ createComponent({ isDeleting: true });
expect(findButton().attributes('disabled')).toBeTruthy();
});
@@ -48,4 +49,18 @@ describe('Batch delete button component', () => {
expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy();
});
});
+
+ it('renders slot content', () => {
+ const testText = 'Archive selected';
+ createComponent(
+ {},
+ {
+ slots: {
+ default: testText,
+ },
+ },
+ );
+
+ expect(findButton().text()).toBe(testText);
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
index b55bacb6fc5..084a7e5d712 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -17,15 +17,15 @@ exports[`Design note component should match the snapshot 1`] = `
/>
<div
- class="d-flex justify-content-between"
+ class="gl-display-flex gl-justify-content-space-between"
>
<div>
- <a
+ <gl-link-stub
class="js-user-link"
data-user-id="author-id"
>
<span
- class="note-header-author-name bold"
+ class="note-header-author-name gl-font-weight-bold"
>
</span>
@@ -37,7 +37,7 @@ exports[`Design note component should match the snapshot 1`] = `
>
@
</span>
- </a>
+ </gl-link-stub>
<span
class="note-headline-light note-headline-meta"
@@ -46,12 +46,21 @@ exports[`Design note component should match the snapshot 1`] = `
class="system-note-message"
/>
- <!---->
+ <gl-link-stub
+ class="note-timestamp system-note-separator gl-display-block gl-mb-2"
+ href="#note_123"
+ >
+ <time-ago-tooltip-stub
+ cssclass=""
+ time="2019-07-26T15:02:20Z"
+ tooltipplacement="bottom"
+ />
+ </gl-link-stub>
</span>
</div>
<div
- class="gl-display-flex"
+ class="gl-display-flex gl-align-items-baseline"
>
<!---->
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
index e01c79e3520..f8c68ca4c83 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
@@ -1,15 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
+"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\">
<!---->
- Comment
-</button>"
+ <!----> <span class=\\"gl-button-text\\">
+ Comment
+ </span></button>"
`;
exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
+"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled gl-button\\">
<!---->
- Save comment
-</button>"
+ <!----> <span class=\\"gl-button-text\\">
+ Save comment
+ </span></button>"
`;
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 176c10ea584..9fbd9b2c2a3 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
@@ -8,8 +8,9 @@ import createNoteMutation from '~/design_management/graphql/mutations/create_not
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
+import mockDiscussion from '../../mock_data/discussion';
-const discussion = {
+const defaultMockDiscussion = {
id: '0',
resolved: false,
resolvable: true,
@@ -31,7 +32,6 @@ describe('Design discussions component', () => {
const mutationVariables = {
mutation: createNoteMutation,
- update: expect.anything(),
variables: {
input: {
noteableId: 'noteable-id',
@@ -40,7 +40,7 @@ describe('Design discussions component', () => {
},
},
};
- const mutate = jest.fn(() => Promise.resolve());
+ const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
const $apollo = {
mutate,
};
@@ -49,7 +49,7 @@ describe('Design discussions component', () => {
wrapper = mount(DesignDiscussion, {
propsData: {
resolvedDiscussionsExpanded: true,
- discussion,
+ discussion: defaultMockDiscussion,
noteableId: 'noteable-id',
designId: 'design-id',
discussionIndex: 1,
@@ -82,7 +82,7 @@ describe('Design discussions component', () => {
beforeEach(() => {
createComponent({
discussion: {
- ...discussion,
+ ...defaultMockDiscussion,
resolvable: false,
},
});
@@ -93,7 +93,7 @@ describe('Design discussions component', () => {
});
it('does not render a checkbox in reply form', () => {
- findReplyPlaceholder().vm.$emit('onMouseDown');
+ findReplyPlaceholder().vm.$emit('onClick');
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().exists()).toBe(false);
@@ -125,7 +125,7 @@ describe('Design discussions component', () => {
it('renders a checkbox with Resolve thread text in reply form', () => {
findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
+ wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Resolve thread');
@@ -141,7 +141,7 @@ describe('Design discussions component', () => {
beforeEach(() => {
createComponent({
discussion: {
- ...discussion,
+ ...defaultMockDiscussion,
resolved: true,
resolvedBy: notes[0].author,
resolvedAt: '2020-05-08T07:10:45Z',
@@ -206,7 +206,7 @@ describe('Design discussions component', () => {
it('renders a checkbox with Unresolve thread text in reply form', () => {
findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
+ wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Unresolve thread');
@@ -218,7 +218,7 @@ describe('Design discussions component', () => {
it('hides reply placeholder and opens form on placeholder click', () => {
createComponent();
findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
+ wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => {
expect(findReplyPlaceholder().exists()).toBe(false);
@@ -226,34 +226,31 @@ describe('Design discussions component', () => {
});
});
- it('calls mutation on submitting form and closes the form', () => {
+ it('calls mutation on submitting form and closes the form', async () => {
createComponent(
- { discussionWithOpenForm: discussion.id },
+ { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
- findReplyForm().vm.$emit('submitForm');
+ findReplyForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalledWith(mutationVariables);
- return mutate()
- .then(() => {
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findReplyForm().exists()).toBe(false);
- });
+ await mutate();
+ await wrapper.vm.$nextTick();
+
+ expect(findReplyForm().exists()).toBe(false);
});
it('clears the discussion comment on closing comment form', () => {
createComponent(
- { discussionWithOpenForm: discussion.id },
+ { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
return wrapper.vm
.$nextTick()
.then(() => {
- findReplyForm().vm.$emit('cancelForm');
+ findReplyForm().vm.$emit('cancel-form');
expect(wrapper.vm.discussionComment).toBe('');
return wrapper.vm.$nextTick();
@@ -263,19 +260,26 @@ describe('Design discussions component', () => {
});
});
- it('applies correct class to design notes when discussion is highlighted', () => {
- createComponent(
- {},
- {
- activeDiscussion: {
- id: notes[0].id,
- source: 'pin',
- },
- },
- );
+ describe('when any note from a discussion is active', () => {
+ it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
+ 'applies correct class to all notes in the active discussion',
+ note => {
+ createComponent(
+ { discussion: mockDiscussion },
+ {
+ activeDiscussion: {
+ id: note.id,
+ source: 'pin',
+ },
+ },
+ );
- expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe(
- true,
+ expect(
+ wrapper
+ .findAll(DesignNote)
+ .wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
+ ).toBe(true);
+ },
);
});
@@ -285,7 +289,7 @@ describe('Design discussions component', () => {
expect(mutate).toHaveBeenCalledWith({
mutation: toggleResolveDiscussionMutation,
variables: {
- id: discussion.id,
+ id: defaultMockDiscussion.id,
resolve: true,
},
});
@@ -296,7 +300,7 @@ describe('Design discussions component', () => {
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
createComponent(
- { discussionWithOpenForm: discussion.id },
+ { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
findResolveButton().trigger('click');
@@ -306,7 +310,7 @@ describe('Design discussions component', () => {
expect(mutate).toHaveBeenCalledWith({
mutation: toggleResolveDiscussionMutation,
variables: {
- id: discussion.id,
+ id: defaultMockDiscussion.id,
resolve: true,
},
});
@@ -317,6 +321,6 @@ describe('Design discussions component', () => {
createComponent();
findReplyPlaceholder().vm.$emit('onClick');
- expect(wrapper.emitted('openForm')).toBeTruthy();
+ expect(wrapper.emitted('open-form')).toBeTruthy();
});
});
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 8b32d3022ee..043091e3dc2 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
@@ -15,6 +15,7 @@ const note = {
userPermissions: {
adminNote: false,
},
+ createdAt: '2019-07-26T15:02:20Z',
};
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
@@ -79,21 +80,10 @@ describe('Design note component', () => {
it('should render a time ago tooltip if note has createdAt property', () => {
createComponent({
- note: {
- ...note,
- createdAt: '2019-07-26T15:02:20Z',
- },
- });
-
- expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
- });
-
- it('should trigger a scrollIntoView method', () => {
- createComponent({
note,
});
- expect(scrollIntoViewMock).toHaveBeenCalled();
+ expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
});
it('should not render edit icon when user does not have a permission', () => {
@@ -143,8 +133,8 @@ describe('Design note component', () => {
expect(findReplyForm().exists()).toBe(true);
});
- it('hides the form on hideForm event', () => {
- findReplyForm().vm.$emit('cancelForm');
+ it('hides the form on cancel-form event', () => {
+ findReplyForm().vm.$emit('cancel-form');
return wrapper.vm.$nextTick().then(() => {
expect(findReplyForm().exists()).toBe(false);
@@ -152,8 +142,8 @@ describe('Design note component', () => {
});
});
- it('calls a mutation on submitForm event and hides a form', () => {
- findReplyForm().vm.$emit('submitForm');
+ it('calls a mutation on submit-form event and hides a form', () => {
+ findReplyForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalled();
return mutate()
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 16b34f150b8..1a80fc4e761 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
@@ -70,7 +70,7 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeFalsy();
+ expect(wrapper.emitted('submit-form')).toBeFalsy();
});
});
@@ -80,20 +80,20 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeFalsy();
+ expect(wrapper.emitted('submit-form')).toBeFalsy();
});
});
it('emits cancelForm event on pressing escape button on textarea', () => {
findTextarea().trigger('keyup.esc');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
it('emits cancelForm event on clicking Cancel button', () => {
findCancelButton().vm.$emit('click');
- expect(wrapper.emitted('cancelForm')).toHaveLength(1);
+ expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
});
@@ -112,7 +112,7 @@ describe('Design reply form component', () => {
findSubmitButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
});
@@ -122,7 +122,7 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
});
@@ -132,7 +132,7 @@ describe('Design reply form component', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
+ expect(wrapper.emitted('submit-form')).toBeTruthy();
});
});
@@ -147,7 +147,7 @@ describe('Design reply form component', () => {
it('emits cancelForm event on Escape key if text was not changed', () => {
findTextarea().trigger('keyup.esc');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
it('opens confirmation modal on Escape key when text has changed', () => {
@@ -162,7 +162,7 @@ describe('Design reply form component', () => {
it('emits cancelForm event on Cancel button click if text was not changed', () => {
findCancelButton().trigger('click');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
it('opens confirmation modal on Cancel button click when text has changed', () => {
@@ -178,7 +178,7 @@ describe('Design reply form component', () => {
findTextarea().trigger('keyup.esc');
findModal().vm.$emit('ok');
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ expect(wrapper.emitted('cancel-form')).toBeTruthy();
});
});
});
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index f243323b162..673a09320e5 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -11,11 +11,11 @@ describe('Design overlay component', () => {
const mockDimensions = { width: 100, height: 100 };
- const findOverlay = () => wrapper.find('.image-diff-overlay');
const findAllNotes = () => wrapper.findAll('.js-image-badge');
const findCommentBadge = () => wrapper.find('.comment-indicator');
- const findFirstBadge = () => findAllNotes().at(0);
- const findSecondBadge = () => findAllNotes().at(1);
+ const findBadgeAtIndex = noteIndex => findAllNotes().at(noteIndex);
+ const findFirstBadge = () => findBadgeAtIndex(0);
+ const findSecondBadge = () => findBadgeAtIndex(1);
const clickAndDragBadge = (elem, fromPoint, toPoint) => {
elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
@@ -56,9 +56,7 @@ describe('Design overlay component', () => {
it('should have correct inline style', () => {
createComponent();
- expect(wrapper.find('.image-diff-overlay').attributes().style).toBe(
- 'width: 100px; height: 100px; top: 0px; left: 0px;',
- );
+ expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;');
});
it('should emit `openCommentForm` when clicking on overlay', () => {
@@ -69,7 +67,7 @@ describe('Design overlay component', () => {
};
wrapper
- .find('.image-diff-overlay-add-comment')
+ .find('[data-qa-selector="design_image_button"]')
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('openCommentForm')).toEqual([
@@ -107,16 +105,43 @@ describe('Design overlay component', () => {
expect(findSecondBadge().classes()).toContain('resolved');
});
- it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
- wrapper.setData({
- activeDiscussion: {
- id: notes[0].id,
- source: 'discussion',
- },
+ describe('when no discussion is active', () => {
+ it('should not apply inactive class to any pins', () => {
+ expect(
+ findAllNotes(0).wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
+ ).toBe(false);
});
+ });
+
+ describe('when a discussion is active', () => {
+ it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
+ 'should not apply inactive class to the pin for the active discussion',
+ note => {
+ wrapper.setData({
+ activeDiscussion: {
+ id: note.id,
+ source: 'discussion',
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
+ });
+ },
+ );
+
+ it('should apply inactive class to all pins besides the active one', () => {
+ wrapper.setData({
+ activeDiscussion: {
+ id: notes[0].id,
+ source: 'discussion',
+ },
+ });
- return wrapper.vm.$nextTick().then(() => {
- expect(findSecondBadge().classes()).toContain('inactive');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findSecondBadge().classes()).toContain('inactive');
+ expect(findFirstBadge().classes()).not.toContain('inactive');
+ });
});
});
});
@@ -309,7 +334,7 @@ describe('Design overlay component', () => {
it.each`
element | getElementFunc | event
- ${'overlay'} | ${findOverlay} | ${'mouseleave'}
+ ${'overlay'} | ${() => wrapper} | ${'mouseleave'}
${'comment badge'} | ${findCommentBadge} | ${'mouseup'}
`(
'should emit `openCommentForm` event when $event fired on $element element',
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index 7e513182589..d633d00f2ed 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -42,7 +42,7 @@ describe('Design management design presentation component', () => {
wrapper.element.scrollTo = jest.fn();
}
- const findOverlayCommentButton = () => wrapper.find('.image-diff-overlay-add-comment');
+ const findOverlayCommentButton = () => wrapper.find('[data-qa-selector="design_image_button"]');
/**
* Spy on $refs and mock given values
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index e098e7de867..700faa8a70f 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -6,6 +6,10 @@ import Participants from '~/sidebar/components/participants/participants.vue';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import design from '../mock_data/design';
import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
+
+const scrollIntoViewMock = jest.fn();
+HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
const updateActiveDiscussionMutationVariables = {
mutation: updateActiveDiscussionMutation,
@@ -39,7 +43,7 @@ describe('Design management design sidebar component', () => {
const findNewDiscussionDisclaimer = () =>
wrapper.find('[data-testid="new-discussion-disclaimer"]');
- function createComponent(props = {}) {
+ function createComponent(props = {}, { enableTodoButton } = {}) {
wrapper = shallowMount(DesignSidebar, {
propsData: {
design,
@@ -53,6 +57,10 @@ describe('Design management design sidebar component', () => {
mutate,
},
},
+ stubs: { GlPopover },
+ provide: {
+ glFeatures: { designManagementTodoButton: enableTodoButton },
+ },
});
}
@@ -146,22 +154,22 @@ describe('Design management design sidebar component', () => {
});
it('emits correct event on discussion create note error', () => {
- findFirstDiscussion().vm.$emit('createNoteError', 'payload');
+ findFirstDiscussion().vm.$emit('create-note-error', 'payload');
expect(wrapper.emitted('onDesignDiscussionError')).toEqual([['payload']]);
});
it('emits correct event on discussion update note error', () => {
- findFirstDiscussion().vm.$emit('updateNoteError', 'payload');
+ findFirstDiscussion().vm.$emit('update-note-error', 'payload');
expect(wrapper.emitted('updateNoteError')).toEqual([['payload']]);
});
it('emits correct event on discussion resolve error', () => {
- findFirstDiscussion().vm.$emit('resolveDiscussionError', 'payload');
+ findFirstDiscussion().vm.$emit('resolve-discussion-error', 'payload');
expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]);
});
it('changes prop correctly on opening discussion form', () => {
- findFirstDiscussion().vm.$emit('openForm', 'some-id');
+ findFirstDiscussion().vm.$emit('open-form', 'some-id');
return wrapper.vm.$nextTick().then(() => {
expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id');
@@ -220,6 +228,10 @@ describe('Design management design sidebar component', () => {
expect(findPopover().exists()).toBe(true);
});
+ it('scrolls to resolved threads link', () => {
+ expect(scrollIntoViewMock).toHaveBeenCalled();
+ });
+
it('dismisses a popover on the outside click', () => {
wrapper.trigger('click');
return wrapper.vm.$nextTick(() => {
@@ -233,4 +245,23 @@ describe('Design management design sidebar component', () => {
expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 });
});
});
+
+ it('does not render To-Do button by default', () => {
+ createComponent();
+ expect(wrapper.find(DesignTodoButton).exists()).toBe(false);
+ });
+
+ describe('when `design_management_todo_button` feature flag is enabled', () => {
+ beforeEach(() => {
+ createComponent({}, { enableTodoButton: true });
+ });
+
+ it('renders sidebar root element with no top padding', () => {
+ expect(wrapper.classes()).toContain('gl-pt-0');
+ });
+
+ it('renders To-Do button', () => {
+ expect(wrapper.find(DesignTodoButton).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
new file mode 100644
index 00000000000..451c23f0fea
--- /dev/null
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -0,0 +1,158 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
+import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
+import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
+import mockDesign from '../mock_data/design';
+
+const mockDesignWithPendingTodos = {
+ ...mockDesign,
+ currentUserTodos: {
+ nodes: [
+ {
+ id: 'todo-id',
+ },
+ ],
+ },
+};
+
+const mutate = jest.fn().mockResolvedValue();
+
+describe('Design management design todo button', () => {
+ let wrapper;
+
+ function createComponent(props = {}, { mountFn = shallowMount } = {}) {
+ wrapper = mountFn(DesignTodoButton, {
+ propsData: {
+ design: mockDesign,
+ ...props,
+ },
+ provide: {
+ projectPath: 'project-path',
+ issueIid: '10',
+ },
+ mocks: {
+ $route: {
+ params: {
+ id: 'my-design.jpg',
+ },
+ query: {},
+ },
+ $apollo: {
+ mutate,
+ },
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ jest.clearAllMocks();
+ });
+
+ it('renders TodoButton component', () => {
+ expect(wrapper.find(TodoButton).exists()).toBe(true);
+ });
+
+ describe('when design has a pending todo', () => {
+ beforeEach(() => {
+ createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount });
+ });
+
+ it('renders correct button text', () => {
+ expect(wrapper.text()).toBe('Mark as done');
+ });
+
+ describe('when clicked', () => {
+ let dispatchEventSpy;
+
+ beforeEach(() => {
+ dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
+ jest.spyOn(document, 'querySelector').mockReturnValue({
+ innerText: 2,
+ });
+
+ createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount });
+ wrapper.trigger('click');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', async () => {
+ const todoMarkDoneMutationVariables = {
+ mutation: todoMarkDoneMutation,
+ update: expect.anything(),
+ variables: {
+ id: 'todo-id',
+ },
+ };
+
+ expect(mutate).toHaveBeenCalledTimes(1);
+ expect(mutate).toHaveBeenCalledWith(todoMarkDoneMutationVariables);
+ });
+
+ it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
+ const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
+
+ expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
+ expect(dispatchedEvent.detail).toEqual({ count: 1 });
+ expect(dispatchedEvent.type).toBe('todo:toggle');
+ });
+ });
+ });
+
+ describe('when design has no pending todos', () => {
+ beforeEach(() => {
+ createComponent({}, { mountFn: mount });
+ });
+
+ it('renders correct button text', () => {
+ expect(wrapper.text()).toBe('Add a To-Do');
+ });
+
+ describe('when clicked', () => {
+ let dispatchEventSpy;
+
+ beforeEach(() => {
+ dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
+ jest.spyOn(document, 'querySelector').mockReturnValue({
+ innerText: 2,
+ });
+
+ createComponent({}, { mountFn: mount });
+ wrapper.trigger('click');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', async () => {
+ const createDesignTodoMutationVariables = {
+ mutation: createDesignTodoMutation,
+ update: expect.anything(),
+ variables: {
+ atVersion: null,
+ filenames: ['my-design.jpg'],
+ designId: '1',
+ issueId: '1',
+ issueIid: '10',
+ projectPath: 'project-path',
+ },
+ };
+
+ expect(mutate).toHaveBeenCalledTimes(1);
+ expect(mutate).toHaveBeenCalledWith(createDesignTodoMutationVariables);
+ });
+
+ it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
+ const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
+
+ expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
+ expect(dispatchedEvent.detail).toEqual({ count: 3 });
+ expect(dispatchedEvent.type).toBe('todo:toggle');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index d76b6e712fe..822df1f6472 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -10,11 +10,11 @@ exports[`Design management list item component when item appears in view after i
exports[`Design management list item component with notes renders item with multiple comments 1`] = `
<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
to="[object Object]"
>
<div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
>
<!---->
@@ -23,7 +23,7 @@ exports[`Design management list item component with notes renders item with mult
<img
alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image"
src=""
/>
@@ -31,13 +31,13 @@ exports[`Design management list item component with notes renders item with mult
</div>
<div
- class="card-footer d-flex w-100"
+ class="card-footer gl-display-flex gl-w-full"
>
<div
- class="d-flex flex-column str-truncated-100"
+ class="gl-display-flex gl-flex-direction-column str-truncated-100"
>
<span
- class="bold str-truncated-100"
+ class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
>
test
@@ -57,17 +57,17 @@ exports[`Design management list item component with notes renders item with mult
</div>
<div
- class="ml-auto d-flex align-items-center text-secondary"
+ class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
>
- <icon-stub
- class="ml-1"
+ <gl-icon-stub
+ class="gl-ml-2"
name="comments"
size="16"
/>
<span
aria-label="2 comments"
- class="ml-1"
+ class="gl-ml-2"
>
2
@@ -80,11 +80,11 @@ exports[`Design management list item component with notes renders item with mult
exports[`Design management list item component with notes renders item with single comment 1`] = `
<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
to="[object Object]"
>
<div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
>
<!---->
@@ -93,7 +93,7 @@ exports[`Design management list item component with notes renders item with sing
<img
alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image"
src=""
/>
@@ -101,13 +101,13 @@ exports[`Design management list item component with notes renders item with sing
</div>
<div
- class="card-footer d-flex w-100"
+ class="card-footer gl-display-flex gl-w-full"
>
<div
- class="d-flex flex-column str-truncated-100"
+ class="gl-display-flex gl-flex-direction-column str-truncated-100"
>
<span
- class="bold str-truncated-100"
+ class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
>
test
@@ -127,17 +127,17 @@ exports[`Design management list item component with notes renders item with sing
</div>
<div
- class="ml-auto d-flex align-items-center text-secondary"
+ class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
>
- <icon-stub
- class="ml-1"
+ <gl-icon-stub
+ class="gl-ml-2"
name="comments"
size="16"
/>
<span
aria-label="1 comment"
- class="ml-1"
+ class="gl-ml-2"
>
1
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index d1c90bd57b0..55c6ecbc26b 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -1,7 +1,6 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import VueRouter from 'vue-router';
-import Icon from '~/vue_shared/components/icon.vue';
import Item from '~/design_management/components/list/item.vue';
const localVue = createLocalVue();
@@ -20,7 +19,7 @@ describe('Design management list item component', () => {
let wrapper;
const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]');
- const findEventIcon = () => findDesignEvent().find(Icon);
+ const findEventIcon = () => findDesignEvent().find(GlIcon);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
function createComponent({
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index d6fd09eb698..1e94e90c3b0 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-new-dropdown-stub
+<gl-dropdown-stub
category="tertiary"
headertext=""
issueiid=""
@@ -10,7 +10,7 @@ exports[`Design management design version dropdown component renders design vers
text="Showing latest version"
variant="default"
>
- <gl-new-dropdown-item-stub
+ <gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
@@ -22,8 +22,8 @@ exports[`Design management design version dropdown component renders design vers
Version
2
(latest)
- </gl-new-dropdown-item-stub>
- <gl-new-dropdown-item-stub
+ </gl-dropdown-item-stub>
+ <gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
@@ -34,12 +34,12 @@ exports[`Design management design version dropdown component renders design vers
Version
1
- </gl-new-dropdown-item-stub>
-</gl-new-dropdown-stub>
+ </gl-dropdown-item-stub>
+</gl-dropdown-stub>
`;
exports[`Design management design version dropdown component renders design version list 1`] = `
-<gl-new-dropdown-stub
+<gl-dropdown-stub
category="tertiary"
headertext=""
issueiid=""
@@ -48,7 +48,7 @@ exports[`Design management design version dropdown component renders design vers
text="Showing latest version"
variant="default"
>
- <gl-new-dropdown-item-stub
+ <gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
@@ -60,8 +60,8 @@ exports[`Design management design version dropdown component renders design vers
Version
2
(latest)
- </gl-new-dropdown-item-stub>
- <gl-new-dropdown-item-stub
+ </gl-dropdown-item-stub>
+ <gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
@@ -72,6 +72,6 @@ exports[`Design management design version dropdown component renders design vers
Version
1
- </gl-new-dropdown-item-stub>
-</gl-new-dropdown-stub>
+ </gl-dropdown-item-stub>
+</gl-dropdown-stub>
`;
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 f4206cdaeb3..4ef787ac754 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
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
import mockAllVersions from './mock_data/all_versions';
@@ -42,7 +42,7 @@ describe('Design management design version dropdown component', () => {
wrapper.destroy();
});
- const findVersionLink = index => wrapper.findAll(GlNewDropdownItem).at(index);
+ const findVersionLink = index => wrapper.findAll(GlDropdownItem).at(index);
it('renders design version dropdown button', () => {
createComponent();
@@ -75,7 +75,7 @@ describe('Design management design version dropdown component', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version');
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
});
});
@@ -83,7 +83,7 @@ describe('Design management design version dropdown component', () => {
createComponent({ maxVersions: 1 });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version');
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
});
});
@@ -91,7 +91,7 @@ describe('Design management design version dropdown component', () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe(`Showing version #1`);
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`);
});
});
@@ -99,7 +99,7 @@ describe('Design management design version dropdown component', () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version');
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
});
});
@@ -107,7 +107,7 @@ describe('Design management design version dropdown component', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlNewDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
+ expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
});
});
});
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index 5e2df3877a5..1c7806c292f 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -13,6 +13,9 @@ export const designListQueryResponse = {
notesCount: 3,
image: 'image-1',
imageV432x230: 'image-1',
+ currentUserTodos: {
+ nodes: [],
+ },
},
{
id: '2',
@@ -21,6 +24,9 @@ export const designListQueryResponse = {
notesCount: 2,
image: 'image-2',
imageV432x230: 'image-2',
+ currentUserTodos: {
+ nodes: [],
+ },
},
{
id: '3',
@@ -29,6 +35,9 @@ export const designListQueryResponse = {
notesCount: 1,
image: 'image-3',
imageV432x230: 'image-3',
+ currentUserTodos: {
+ nodes: [],
+ },
},
],
},
@@ -60,6 +69,9 @@ export const reorderedDesigns = [
notesCount: 2,
image: 'image-2',
imageV432x230: 'image-2',
+ currentUserTodos: {
+ nodes: [],
+ },
},
{
id: '1',
@@ -68,6 +80,9 @@ export const reorderedDesigns = [
notesCount: 3,
image: 'image-1',
imageV432x230: 'image-1',
+ currentUserTodos: {
+ nodes: [],
+ },
},
{
id: '3',
@@ -76,6 +91,9 @@ export const reorderedDesigns = [
notesCount: 1,
image: 'image-3',
imageV432x230: 'image-3',
+ currentUserTodos: {
+ nodes: [],
+ },
},
];
diff --git a/spec/frontend/design_management/mock_data/design.js b/spec/frontend/design_management/mock_data/design.js
index 72be33fef1d..f2a3a800969 100644
--- a/spec/frontend/design_management/mock_data/design.js
+++ b/spec/frontend/design_management/mock_data/design.js
@@ -1,5 +1,5 @@
export default {
- id: 'design-id',
+ id: 'gid::/gitlab/Design/1',
filename: 'test.jpg',
fullPath: 'full-design-path',
image: 'test.jpg',
@@ -8,6 +8,7 @@ export default {
name: 'test',
},
issue: {
+ id: 'gid::/gitlab/Issue/1',
title: 'My precious issue',
webPath: 'full-issue-path',
webUrl: 'full-issue-url',
diff --git a/spec/frontend/design_management/mock_data/discussion.js b/spec/frontend/design_management/mock_data/discussion.js
new file mode 100644
index 00000000000..fbf9a2fdcc1
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/discussion.js
@@ -0,0 +1,45 @@
+export default {
+ id: 'discussion-id-1',
+ resolved: false,
+ resolvable: true,
+ notes: [
+ {
+ id: 'note-id-1',
+ index: 1,
+ position: {
+ height: 100,
+ width: 100,
+ x: 10,
+ y: 15,
+ },
+ author: {
+ name: 'John',
+ webUrl: 'link-to-john-profile',
+ },
+ createdAt: '2020-05-08T07:10:45Z',
+ userPermissions: {
+ adminNote: true,
+ },
+ resolved: false,
+ },
+ {
+ id: 'note-id-3',
+ index: 3,
+ position: {
+ height: 50,
+ width: 50,
+ x: 25,
+ y: 25,
+ },
+ author: {
+ name: 'Mary',
+ webUrl: 'link-to-mary-profile',
+ },
+ createdAt: '2020-05-08T07:10:45Z',
+ userPermissions: {
+ adminNote: true,
+ },
+ resolved: false,
+ },
+ ],
+};
diff --git a/spec/frontend/design_management/mock_data/notes.js b/spec/frontend/design_management/mock_data/notes.js
index 80cb3944786..41cefaca05b 100644
--- a/spec/frontend/design_management/mock_data/notes.js
+++ b/spec/frontend/design_management/mock_data/notes.js
@@ -1,46 +1,44 @@
+import DISCUSSION_1 from './discussion';
+
+const DISCUSSION_2 = {
+ id: 'discussion-id-2',
+ notes: {
+ nodes: [
+ {
+ id: 'note-id-2',
+ index: 2,
+ position: {
+ height: 50,
+ width: 50,
+ x: 25,
+ y: 25,
+ },
+ author: {
+ name: 'Mary',
+ webUrl: 'link-to-mary-profile',
+ },
+ createdAt: '2020-05-08T07:10:45Z',
+ userPermissions: {
+ adminNote: true,
+ },
+ resolved: true,
+ },
+ ],
+ },
+};
+
export default [
{
- id: 'note-id-1',
- index: 1,
- position: {
- height: 100,
- width: 100,
- x: 10,
- y: 15,
- },
- author: {
- name: 'John',
- webUrl: 'link-to-john-profile',
- },
- createdAt: '2020-05-08T07:10:45Z',
- userPermissions: {
- adminNote: true,
- },
+ ...DISCUSSION_1.notes[0],
discussion: {
- id: 'discussion-id-1',
+ id: DISCUSSION_1.id,
+ notes: {
+ nodes: DISCUSSION_1.notes,
+ },
},
- resolved: false,
},
{
- id: 'note-id-2',
- index: 2,
- position: {
- height: 50,
- width: 50,
- x: 25,
- y: 25,
- },
- author: {
- name: 'Mary',
- webUrl: 'link-to-mary-profile',
- },
- createdAt: '2020-05-08T07:10:45Z',
- userPermissions: {
- adminNote: true,
- },
- discussion: {
- id: 'discussion-id-2',
- },
- resolved: true,
+ ...DISCUSSION_2.notes.nodes[0],
+ discussion: DISCUSSION_2,
},
];
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index 3881b2d7679..b80b7fdb43e 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -111,7 +111,7 @@ exports[`Design management index page designs renders designs list and header wi
>
<gl-button-stub
category="primary"
- class="gl-mr-3 js-select-all"
+ class="gl-mr-4 js-select-all"
icon=""
size="small"
variant="link"
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 823294efc38..c849e4d4ed6 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -32,6 +32,8 @@ exports[`Design management design index page renders design index 1`] = `
<div
class="image-notes"
>
+ <!---->
+
<h2
class="gl-font-weight-bold gl-mt-0"
>
@@ -57,11 +59,11 @@ exports[`Design management design index page renders design index 1`] = `
<design-discussion-stub
data-testid="unresolved-discussion"
- designid="test"
+ designid="gid::/gitlab/Design/1"
discussion="[object Object]"
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
- noteableid="design-id"
+ noteableid="gid::/gitlab/Design/1"
/>
<gl-button-stub
@@ -92,7 +94,7 @@ exports[`Design management design index page renders design index 1`] = `
</p>
<a
- href="#"
+ href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads"
rel="noopener noreferrer"
target="_blank"
>
@@ -105,11 +107,11 @@ exports[`Design management design index page renders design index 1`] = `
>
<design-discussion-stub
data-testid="resolved-discussion"
- designid="test"
+ designid="gid::/gitlab/Design/1"
discussion="[object Object]"
discussionwithopenform=""
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
- noteableid="design-id"
+ noteableid="gid::/gitlab/Design/1"
/>
</gl-collapse-stub>
@@ -179,6 +181,8 @@ exports[`Design management design index page with error GlAlert is rendered in c
<div
class="image-notes"
>
+ <!---->
+
<h2
class="gl-font-weight-bold gl-mt-0"
>
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 369c8667f4d..d9f7146d258 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -7,24 +7,21 @@ import DesignIndex from '~/design_management/pages/design/index.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import DesignPresentation from '~/design_management/components/design_presentation.vue';
import createImageDiffNoteMutation from '~/design_management/graphql/mutations/create_image_diff_note.mutation.graphql';
-import design from '../../mock_data/design';
-import mockResponseWithDesigns from '../../mock_data/designs';
-import mockResponseNoDesigns from '../../mock_data/no_designs';
-import mockAllVersions from '../../mock_data/all_versions';
+import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
import {
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
} from '~/design_management/utils/error_messages';
-import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
+import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
import createRouter from '~/design_management/router';
import * as utils from '~/design_management/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
+import design from '../../mock_data/design';
+import mockResponseWithDesigns from '../../mock_data/designs';
+import mockResponseNoDesigns from '../../mock_data/no_designs';
+import mockAllVersions from '../../mock_data/all_versions';
jest.mock('~/flash');
-jest.mock('mousetrap', () => ({
- bind: jest.fn(),
- unbind: jest.fn(),
-}));
const focusInput = jest.fn();
@@ -34,6 +31,12 @@ const DesignReplyForm = {
focusInput,
},
};
+const mockDesignNoDiscussions = {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+};
const localVue = createLocalVue();
localVue.use(VueRouter);
@@ -75,7 +78,7 @@ describe('Design management design index page', () => {
const findSidebar = () => wrapper.find(DesignSidebar);
const findDesignPresentation = () => wrapper.find(DesignPresentation);
- function createComponent(loading = false, data = {}) {
+ function createComponent({ loading = false } = {}, { data = {}, intialRouteOptions = {} } = {}) {
const $apollo = {
queries: {
design: {
@@ -87,6 +90,8 @@ describe('Design management design index page', () => {
router = createRouter();
+ router.push({ name: DESIGN_ROUTE_NAME, params: { id: design.id }, ...intialRouteOptions });
+
wrapper = shallowMount(DesignIndex, {
propsData: { id: '1' },
mocks: { $apollo },
@@ -126,29 +131,28 @@ describe('Design management design index page', () => {
},
};
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl);
- createComponent(true);
+ createComponent({ loading: true });
- wrapper.vm.$router.push('/designs/test');
expect(mockEl.classList.add).toHaveBeenCalledTimes(1);
expect(mockEl.classList.add).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
});
});
it('sets loading state', () => {
- createComponent(true);
+ createComponent({ loading: true });
expect(wrapper.element).toMatchSnapshot();
});
it('renders design index', () => {
- createComponent(false, { design });
+ createComponent({ loading: false }, { data: { design } });
expect(wrapper.element).toMatchSnapshot();
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
it('passes correct props to sidebar component', () => {
- createComponent(false, { design });
+ createComponent({ loading: false }, { data: { design } });
expect(findSidebar().props()).toEqual({
design,
@@ -158,14 +162,14 @@ describe('Design management design index page', () => {
});
it('opens a new discussion form', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
+ createComponent(
+ { loading: false },
+ {
+ data: {
+ design,
},
},
- });
+ );
findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 });
@@ -175,15 +179,15 @@ describe('Design management design index page', () => {
});
it('keeps new discussion form focused', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
+ createComponent(
+ { loading: false },
+ {
+ data: {
+ design,
+ annotationCoordinates,
},
},
- annotationCoordinates,
- });
+ );
findDesignPresentation().vm.$emit('openCommentForm', { x: 10, y: 10 });
@@ -191,18 +195,18 @@ describe('Design management design index page', () => {
});
it('sends a mutation on submitting form and closes form', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
+ createComponent(
+ { loading: false },
+ {
+ data: {
+ design,
+ annotationCoordinates,
+ comment: newComment,
},
},
- annotationCoordinates,
- comment: newComment,
- });
+ );
- findDiscussionForm().vm.$emit('submitForm');
+ findDiscussionForm().vm.$emit('submit-form');
expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables);
return wrapper.vm
@@ -216,18 +220,18 @@ describe('Design management design index page', () => {
});
it('closes the form and clears the comment on canceling form', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
+ createComponent(
+ { loading: false },
+ {
+ data: {
+ design,
+ annotationCoordinates,
+ comment: newComment,
},
},
- annotationCoordinates,
- comment: newComment,
- });
+ );
- findDiscussionForm().vm.$emit('cancelForm');
+ findDiscussionForm().vm.$emit('cancel-form');
expect(wrapper.vm.comment).toBe('');
@@ -238,15 +242,15 @@ describe('Design management design index page', () => {
describe('with error', () => {
beforeEach(() => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
+ createComponent(
+ { loading: false },
+ {
+ data: {
+ design: mockDesignNoDiscussions,
+ errorMessage: 'woops',
},
},
- errorMessage: 'woops',
- });
+ );
});
it('GlAlert is rendered in correct position with correct content', () => {
@@ -257,7 +261,7 @@ describe('Design management design index page', () => {
describe('onDesignQueryResult', () => {
describe('with no designs', () => {
it('redirects to /designs', () => {
- createComponent(true);
+ createComponent({ loading: true });
router.push = jest.fn();
wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
@@ -272,7 +276,7 @@ describe('Design management design index page', () => {
describe('when no design exists for given version', () => {
it('redirects to /designs', () => {
- createComponent(true);
+ createComponent({ loading: true });
wrapper.setData({
allVersions: mockAllVersions,
});
@@ -291,4 +295,24 @@ describe('Design management design index page', () => {
});
});
});
+
+ describe('when hash present in current route', () => {
+ it('calls updateActiveDiscussion mutation', () => {
+ createComponent(
+ { loading: false },
+ {
+ data: {
+ design,
+ },
+ intialRouteOptions: { hash: '#note_123' },
+ },
+ );
+
+ expect(mutate).toHaveBeenCalledTimes(1);
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: updateActiveDiscussion,
+ variables: { id: 'gid://gitlab/DiffNote/123', source: 'url' },
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/pages/index_apollo_spec.js b/spec/frontend/design_management/pages/index_apollo_spec.js
deleted file mode 100644
index 3ea711c2cfa..00000000000
--- a/spec/frontend/design_management/pages/index_apollo_spec.js
+++ /dev/null
@@ -1,162 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { createMockClient } from 'mock-apollo-client';
-import VueApollo from 'vue-apollo';
-import VueRouter from 'vue-router';
-import VueDraggable from 'vuedraggable';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import Design from '~/design_management/components/list/item.vue';
-import createRouter from '~/design_management/router';
-import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
-import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
-import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Index from '~/design_management/pages/index.vue';
-import {
- designListQueryResponse,
- permissionsQueryResponse,
- moveDesignMutationResponse,
- reorderedDesigns,
- moveDesignMutationResponseWithErrors,
-} from '../mock_data/apollo_mock';
-
-jest.mock('~/flash');
-
-const localVue = createLocalVue();
-localVue.use(VueApollo);
-
-const router = createRouter();
-localVue.use(VueRouter);
-
-const designToMove = {
- __typename: 'Design',
- id: '2',
- event: 'NONE',
- filename: 'fox_2.jpg',
- notesCount: 2,
- image: 'image-2',
- imageV432x230: 'image-2',
-};
-
-describe('Design management index page with Apollo mock', () => {
- let wrapper;
- let mockClient;
- let apolloProvider;
- let moveDesignHandler;
-
- async function moveDesigns(localWrapper) {
- await jest.runOnlyPendingTimers();
- await localWrapper.vm.$nextTick();
-
- localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
- localWrapper.find(VueDraggable).vm.$emit('change', {
- moved: {
- newIndex: 0,
- element: designToMove,
- },
- });
- }
-
- const fragmentMatcher = { match: () => true };
-
- const cache = new InMemoryCache({
- fragmentMatcher,
- addTypename: false,
- });
-
- const findDesigns = () => wrapper.findAll(Design);
-
- function createComponent({
- moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
- }) {
- mockClient = createMockClient({ cache });
-
- mockClient.setRequestHandler(
- getDesignListQuery,
- jest.fn().mockResolvedValue(designListQueryResponse),
- );
-
- mockClient.setRequestHandler(
- permissionsQuery,
- jest.fn().mockResolvedValue(permissionsQueryResponse),
- );
-
- moveDesignHandler = moveHandler;
-
- mockClient.setRequestHandler(moveDesignMutation, moveDesignHandler);
-
- apolloProvider = new VueApollo({
- defaultClient: mockClient,
- });
-
- wrapper = shallowMount(Index, {
- localVue,
- apolloProvider,
- router,
- stubs: { VueDraggable },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- mockClient = null;
- apolloProvider = null;
- });
-
- it('has a design with id 1 as a first one', async () => {
- createComponent({});
-
- await jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
-
- expect(findDesigns()).toHaveLength(3);
- expect(
- findDesigns()
- .at(0)
- .props('id'),
- ).toBe('1');
- });
-
- it('calls a mutation with correct parameters and reorders designs', async () => {
- createComponent({});
-
- await moveDesigns(wrapper);
-
- expect(moveDesignHandler).toHaveBeenCalled();
-
- await wrapper.vm.$nextTick();
-
- expect(
- findDesigns()
- .at(0)
- .props('id'),
- ).toBe('2');
- });
-
- it('displays flash if mutation had a recoverable error', async () => {
- createComponent({
- moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
- });
-
- await moveDesigns(wrapper);
-
- await wrapper.vm.$nextTick();
-
- expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem');
- });
-
- it('displays flash if mutation had a non-recoverable error', async () => {
- createComponent({
- moveHandler: jest.fn().mockRejectedValue('Error'),
- });
-
- await moveDesigns(wrapper);
-
- await jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
-
- expect(createFlash).toHaveBeenCalledWith(
- 'Something went wrong when reordering designs. Please try again',
- );
- });
-});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 093fa155d2e..661717d29a3 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -1,13 +1,15 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
+import VueApollo, { ApolloMutation } from 'vue-apollo';
import VueDraggable from 'vuedraggable';
import VueRouter from 'vue-router';
import { GlEmptyState } from '@gitlab/ui';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
import Index from '~/design_management/pages/index.vue';
import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue';
import DeleteButton from '~/design_management/components/delete_button.vue';
+import Design from '~/design_management/components/list/item.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
import {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
@@ -17,6 +19,16 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import createRouter from '~/design_management/router';
import * as utils from '~/design_management/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
+import {
+ designListQueryResponse,
+ permissionsQueryResponse,
+ moveDesignMutationResponse,
+ reorderedDesigns,
+ moveDesignMutationResponseWithErrors,
+} from '../mock_data/apollo_mock';
+import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
+import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
+import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
jest.mock('~/flash.js');
const mockPageEl = {
@@ -61,9 +73,21 @@ const mockVersion = {
id: 'gid://gitlab/DesignManagement::Version/1',
};
+const designToMove = {
+ __typename: 'Design',
+ id: '2',
+ event: 'NONE',
+ filename: 'fox_2.jpg',
+ notesCount: 2,
+ image: 'image-2',
+ imageV432x230: 'image-2',
+};
+
describe('Design management index page', () => {
let mutate;
let wrapper;
+ let fakeApollo;
+ let moveDesignHandler;
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
const findSelectAllButton = () => wrapper.find('.js-select-all');
@@ -74,6 +98,20 @@ describe('Design management index page', () => {
const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
+ const findDesigns = () => wrapper.findAll(Design);
+
+ async function moveDesigns(localWrapper) {
+ await jest.runOnlyPendingTimers();
+ await localWrapper.vm.$nextTick();
+
+ localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
+ localWrapper.find(VueDraggable).vm.$emit('change', {
+ moved: {
+ newIndex: 0,
+ element: designToMove,
+ },
+ });
+ }
function createComponent({
loading = false,
@@ -118,8 +156,30 @@ describe('Design management index page', () => {
});
}
+ function createComponentWithApollo({
+ moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
+ }) {
+ localVue.use(VueApollo);
+ moveDesignHandler = moveHandler;
+
+ const requestHandlers = [
+ [getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
+ [permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
+ [moveDesignMutation, moveDesignHandler],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+ wrapper = shallowMount(Index, {
+ localVue,
+ apolloProvider: fakeApollo,
+ router,
+ stubs: { VueDraggable },
+ });
+ }
+
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
describe('designs', () => {
@@ -478,16 +538,15 @@ describe('Design management index page', () => {
describe('on non-latest version', () => {
beforeEach(() => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+ });
- router.replace({
+ it('does not render design checkboxes', async () => {
+ await router.replace({
name: DESIGNS_ROUTE_NAME,
query: {
version: '2',
},
});
- });
-
- it('does not render design checkboxes', () => {
expect(findDesignCheckboxes()).toHaveLength(0);
});
@@ -514,13 +573,6 @@ describe('Design management index page', () => {
files: [{ name: 'image.png', type: 'image/png' }],
getData: () => 'test.png',
};
-
- router.replace({
- name: DESIGNS_ROUTE_NAME,
- query: {
- version: '2',
- },
- });
});
it('does not call paste event if designs wrapper is not hovered', () => {
@@ -587,7 +639,69 @@ describe('Design management index page', () => {
});
createComponent(true);
- expect(scrollIntoViewMock).toHaveBeenCalled();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(scrollIntoViewMock).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('with mocked Apollo client', () => {
+ it('has a design with id 1 as a first one', async () => {
+ createComponentWithApollo({});
+
+ await jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ expect(findDesigns()).toHaveLength(3);
+ expect(
+ findDesigns()
+ .at(0)
+ .props('id'),
+ ).toBe('1');
+ });
+
+ it('calls a mutation with correct parameters and reorders designs', async () => {
+ createComponentWithApollo({});
+
+ await moveDesigns(wrapper);
+
+ expect(moveDesignHandler).toHaveBeenCalled();
+
+ await wrapper.vm.$nextTick();
+
+ expect(
+ findDesigns()
+ .at(0)
+ .props('id'),
+ ).toBe('2');
+ });
+
+ it('displays flash if mutation had a recoverable error', async () => {
+ createComponentWithApollo({
+ moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
+ });
+
+ await moveDesigns(wrapper);
+
+ await wrapper.vm.$nextTick();
+
+ expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem');
+ });
+
+ it('displays flash if mutation had a non-recoverable error', async () => {
+ createComponentWithApollo({
+ moveHandler: jest.fn().mockRejectedValue('Error'),
+ });
+
+ await moveDesigns(wrapper);
+
+ await wrapper.vm.$nextTick(); // kick off the DOM update
+ await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
+ await wrapper.vm.$nextTick(); // kick off the DOM update for flash
+
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong when reordering designs. Please try again',
+ );
});
});
});
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index 2b8c7ee959b..d4cb9f75a77 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -35,11 +35,6 @@ function factory(routeArg) {
});
}
-jest.mock('mousetrap', () => ({
- bind: jest.fn(),
- unbind: jest.fn(),
-}));
-
describe('Design management router', () => {
afterEach(() => {
window.location.hash = '';
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index e8a5cf3949d..6c859e8c3e8 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -1,14 +1,12 @@
import { InMemoryCache } from 'apollo-cache-inmemory';
import {
updateStoreAfterDesignsDelete,
- updateStoreAfterAddDiscussionComment,
updateStoreAfterAddImageDiffNote,
updateStoreAfterUploadDesign,
updateStoreAfterUpdateImageDiffNote,
} from '~/design_management/utils/cache_update';
import {
designDeletionError,
- ADD_DISCUSSION_COMMENT_ERROR,
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
} from '~/design_management/utils/error_messages';
@@ -28,12 +26,11 @@ describe('Design Management cache update', () => {
describe('error handling', () => {
it.each`
- fnName | subject | errorMessage | extraArgs
- ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
- ${'updateStoreAfterAddDiscussionComment'} | ${updateStoreAfterAddDiscussionComment} | ${ADD_DISCUSSION_COMMENT_ERROR} | ${[]}
- ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
- ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
- ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
+ fnName | subject | errorMessage | extraArgs
+ ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
+ ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
+ ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
+ ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
`('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => {
expect(createFlash).not.toHaveBeenCalled();
expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow();
diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js
index e6d836b9157..7e857d08d25 100644
--- a/spec/frontend/design_management/utils/design_management_utils_spec.js
+++ b/spec/frontend/design_management/utils/design_management_utils_spec.js
@@ -6,6 +6,7 @@ import {
updateImageDiffNoteOptimisticResponse,
isValidDesignFile,
extractDesign,
+ extractDesignNoteId,
} from '~/design_management/utils/design_management_utils';
import mockResponseNoDesigns from '../mock_data/no_designs';
import mockResponseWithDesigns from '../mock_data/designs';
@@ -171,3 +172,19 @@ describe('extractDesign', () => {
});
});
});
+
+describe('extractDesignNoteId', () => {
+ it.each`
+ hash | expectedNoteId
+ ${'#note_0'} | ${'0'}
+ ${'#note_1'} | ${'1'}
+ ${'#note_23'} | ${'23'}
+ ${'#note_456'} | ${'456'}
+ ${'note_1'} | ${null}
+ ${'#note_'} | ${null}
+ ${'#note_asd'} | ${null}
+ ${'#note_1asd'} | ${null}
+ `('returns $expectedNoteId when hash is $hash', ({ hash, expectedNoteId }) => {
+ expect(extractDesignNoteId(hash)).toBe(expectedNoteId);
+ });
+});
diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap
deleted file mode 100644
index 62a0f675cff..00000000000
--- a/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design note pin component should match the snapshot of note when repositioning 1`] = `
-<button
- aria-label="Comment form position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator"
- style="left: 10px; top: 10px; cursor: move;"
- type="button"
->
- <gl-icon-stub
- name="image-comment-dark"
- size="24"
- />
-</button>
-`;
-
-exports[`Design note pin component should match the snapshot of note with index 1`] = `
-<button
- aria-label="Comment '1' position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 js-image-badge badge badge-pill"
- style="left: 10px; top: 10px;"
- type="button"
->
-
- 1
-
-</button>
-`;
-
-exports[`Design note pin component should match the snapshot of note without index 1`] = `
-<button
- aria-label="Comment form position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator"
- style="left: 10px; top: 10px;"
- type="button"
->
- <gl-icon-stub
- name="image-comment-dark"
- size="24"
- />
-</button>
-`;
diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap
deleted file mode 100644
index 189962c5b2e..00000000000
--- a/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap
+++ /dev/null
@@ -1,104 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management design presentation component currentCommentForm is equal to current annotation position when isAnnotating is true 1`] = `
-<div
- class="h-100 w-100 p-3 overflow-auto position-relative"
->
- <div
- class="h-100 w-100 d-flex align-items-center position-relative"
- >
- <design-image-stub
- image="test.jpg"
- name="test"
- scale="1"
- />
-
- <design-overlay-stub
- currentcommentform="[object Object]"
- dimensions="[object Object]"
- notes=""
- position="[object Object]"
- />
- </div>
-</div>
-`;
-
-exports[`Design management design presentation component currentCommentForm is null when isAnnotating is false 1`] = `
-<div
- class="h-100 w-100 p-3 overflow-auto position-relative"
->
- <div
- class="h-100 w-100 d-flex align-items-center position-relative"
- >
- <design-image-stub
- image="test.jpg"
- name="test"
- scale="1"
- />
-
- <design-overlay-stub
- dimensions="[object Object]"
- notes=""
- position="[object Object]"
- />
- </div>
-</div>
-`;
-
-exports[`Design management design presentation component currentCommentForm is null when isAnnotating is true but annotation position is falsey 1`] = `
-<div
- class="h-100 w-100 p-3 overflow-auto position-relative"
->
- <div
- class="h-100 w-100 d-flex align-items-center position-relative"
- >
- <design-image-stub
- image="test.jpg"
- name="test"
- scale="1"
- />
-
- <design-overlay-stub
- dimensions="[object Object]"
- notes=""
- position="[object Object]"
- />
- </div>
-</div>
-`;
-
-exports[`Design management design presentation component renders empty state when no image provided 1`] = `
-<div
- class="h-100 w-100 p-3 overflow-auto position-relative"
->
- <div
- class="h-100 w-100 d-flex align-items-center position-relative"
- >
- <!---->
-
- <!---->
- </div>
-</div>
-`;
-
-exports[`Design management design presentation component renders image and overlay when image provided 1`] = `
-<div
- class="h-100 w-100 p-3 overflow-auto position-relative"
->
- <div
- class="h-100 w-100 d-flex align-items-center position-relative"
- >
- <design-image-stub
- image="test.jpg"
- name="test"
- scale="1"
- />
-
- <design-overlay-stub
- dimensions="[object Object]"
- notes=""
- position="[object Object]"
- />
- </div>
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap
deleted file mode 100644
index cb4575cbd11..00000000000
--- a/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap
+++ /dev/null
@@ -1,115 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management design scaler component minus and reset buttons are disabled when scale === 1 1`] = `
-<div
- class="design-scaler btn-group"
- role="group"
->
- <button
- class="btn"
- disabled="disabled"
- >
- <span
- class="d-flex-center gl-icon s16"
- >
-
- –
-
- </span>
- </button>
-
- <button
- class="btn"
- disabled="disabled"
- >
- <gl-icon-stub
- name="redo"
- size="16"
- />
- </button>
-
- <button
- class="btn"
- >
- <gl-icon-stub
- name="plus"
- size="16"
- />
- </button>
-</div>
-`;
-
-exports[`Design management design scaler component minus and reset buttons are enabled when scale > 1 1`] = `
-<div
- class="design-scaler btn-group"
- role="group"
->
- <button
- class="btn"
- >
- <span
- class="d-flex-center gl-icon s16"
- >
-
- –
-
- </span>
- </button>
-
- <button
- class="btn"
- >
- <gl-icon-stub
- name="redo"
- size="16"
- />
- </button>
-
- <button
- class="btn"
- >
- <gl-icon-stub
- name="plus"
- size="16"
- />
- </button>
-</div>
-`;
-
-exports[`Design management design scaler component plus button is disabled when scale === 2 1`] = `
-<div
- class="design-scaler btn-group"
- role="group"
->
- <button
- class="btn"
- >
- <span
- class="d-flex-center gl-icon s16"
- >
-
- –
-
- </span>
- </button>
-
- <button
- class="btn"
- >
- <gl-icon-stub
- name="redo"
- size="16"
- />
- </button>
-
- <button
- class="btn"
- disabled="disabled"
- >
- <gl-icon-stub
- name="plus"
- size="16"
- />
- </button>
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap
deleted file mode 100644
index acaa62b11eb..00000000000
--- a/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap
+++ /dev/null
@@ -1,68 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management large image component renders image 1`] = `
-<div
- class="m-auto js-design-image"
->
- <!---->
-
- <img
- alt="test"
- class="mh-100 img-fluid"
- src="test.jpg"
- />
-</div>
-`;
-
-exports[`Design management large image component renders loading state 1`] = `
-<div
- class="m-auto js-design-image"
- isloading="true"
->
- <!---->
-
- <img
- alt=""
- class="mh-100 img-fluid"
- src=""
- />
-</div>
-`;
-
-exports[`Design management large image component renders media broken icon on error 1`] = `
-<gl-icon-stub
- class="text-secondary-100"
- name="media-broken"
- size="48"
-/>
-`;
-
-exports[`Design management large image component sets correct classes and styles if imageStyle is set 1`] = `
-<div
- class="m-auto js-design-image"
->
- <!---->
-
- <img
- alt="test"
- class="mh-100"
- src="test.jpg"
- style="width: 100px; height: 100px;"
- />
-</div>
-`;
-
-exports[`Design management large image component zoom sets image style when zoomed 1`] = `
-<div
- class="m-auto js-design-image"
->
- <!---->
-
- <img
- alt="test"
- class="mh-100"
- src="test.jpg"
- style="width: 200px; height: 200px;"
- />
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/components/delete_button_spec.js b/spec/frontend/design_management_legacy/components/delete_button_spec.js
deleted file mode 100644
index 73b4908d06a..00000000000
--- a/spec/frontend/design_management_legacy/components/delete_button_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
-import BatchDeleteButton from '~/design_management_legacy/components/delete_button.vue';
-
-describe('Batch delete button component', () => {
- let wrapper;
-
- const findButton = () => wrapper.find(GlDeprecatedButton);
- const findModal = () => wrapper.find(GlModal);
-
- function createComponent(isDeleting = false) {
- wrapper = shallowMount(BatchDeleteButton, {
- propsData: {
- isDeleting,
- },
- directives: {
- GlModalDirective,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders non-disabled button by default', () => {
- createComponent();
-
- expect(findButton().exists()).toBe(true);
- expect(findButton().attributes('disabled')).toBeFalsy();
- });
-
- it('renders disabled button when design is deleting', () => {
- createComponent(true);
- expect(findButton().attributes('disabled')).toBeTruthy();
- });
-
- it('emits `deleteSelectedDesigns` event on modal ok click', () => {
- createComponent();
- findButton().vm.$emit('click');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findModal().vm.$emit('ok');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy();
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_note_pin_spec.js b/spec/frontend/design_management_legacy/components/design_note_pin_spec.js
deleted file mode 100644
index 3077928cf86..00000000000
--- a/spec/frontend/design_management_legacy/components/design_note_pin_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import DesignNotePin from '~/design_management_legacy/components/design_note_pin.vue';
-
-describe('Design note pin component', () => {
- let wrapper;
-
- function createComponent(propsData = {}) {
- wrapper = shallowMount(DesignNotePin, {
- propsData: {
- position: {
- left: '10px',
- top: '10px',
- },
- ...propsData,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should match the snapshot of note without index', () => {
- createComponent();
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('should match the snapshot of note with index', () => {
- createComponent({ label: 1 });
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('should match the snapshot of note when repositioning', () => {
- createComponent({ repositioning: true });
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('pinStyle', () => {
- it('sets cursor to `move` when repositioning = true', () => {
- createComponent({ repositioning: true });
- expect(wrapper.vm.pinStyle.cursor).toBe('move');
- });
-
- it('does not set cursor when repositioning = false', () => {
- createComponent();
- expect(wrapper.vm.pinStyle.cursor).toBe(undefined);
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap
deleted file mode 100644
index b55bacb6fc5..00000000000
--- a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ /dev/null
@@ -1,67 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design note component should match the snapshot 1`] = `
-<timeline-entry-item-stub
- class="design-note note-form"
- id="note_123"
->
- <user-avatar-link-stub
- imgalt=""
- imgcssclasses=""
- imgsize="40"
- imgsrc=""
- linkhref=""
- tooltipplacement="top"
- tooltiptext=""
- username=""
- />
-
- <div
- class="d-flex justify-content-between"
- >
- <div>
- <a
- class="js-user-link"
- data-user-id="author-id"
- >
- <span
- class="note-header-author-name bold"
- >
-
- </span>
-
- <!---->
-
- <span
- class="note-headline-light"
- >
- @
- </span>
- </a>
-
- <span
- class="note-headline-light note-headline-meta"
- >
- <span
- class="system-note-message"
- />
-
- <!---->
- </span>
- </div>
-
- <div
- class="gl-display-flex"
- >
-
- <!---->
- </div>
- </div>
-
- <div
- class="note-text js-note-text md"
- data-qa-selector="note_content"
- />
-
-</timeline-entry-item-stub>
-`;
diff --git a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
deleted file mode 100644
index e01c79e3520..00000000000
--- a/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ /dev/null
@@ -1,15 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
- <!---->
- Comment
-</button>"
-`;
-
-exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
- <!---->
- Save comment
-</button>"
-`;
diff --git a/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js
deleted file mode 100644
index d20be97f470..00000000000
--- a/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js
+++ /dev/null
@@ -1,318 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
-import notes from '../../mock_data/notes';
-import DesignDiscussion from '~/design_management_legacy/components/design_notes/design_discussion.vue';
-import DesignNote from '~/design_management_legacy/components/design_notes/design_note.vue';
-import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue';
-import createNoteMutation from '~/design_management_legacy/graphql/mutations/create_note.mutation.graphql';
-import toggleResolveDiscussionMutation from '~/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
-import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import ToggleRepliesWidget from '~/design_management_legacy/components/design_notes/toggle_replies_widget.vue';
-
-const discussion = {
- id: '0',
- resolved: false,
- resolvable: true,
- notes,
-};
-
-describe('Design discussions component', () => {
- let wrapper;
-
- const findDesignNotes = () => wrapper.findAll(DesignNote);
- const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder);
- const findReplyForm = () => wrapper.find(DesignReplyForm);
- const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget);
- const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]');
- const findResolveIcon = () => wrapper.find('[data-testid="resolve-icon"]');
- const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
- const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
-
- const mutationVariables = {
- mutation: createNoteMutation,
- update: expect.anything(),
- variables: {
- input: {
- noteableId: 'noteable-id',
- body: 'test',
- discussionId: '0',
- },
- },
- };
- const mutate = jest.fn(() => Promise.resolve());
- const $apollo = {
- mutate,
- };
-
- function createComponent(props = {}, data = {}) {
- wrapper = mount(DesignDiscussion, {
- propsData: {
- resolvedDiscussionsExpanded: true,
- discussion,
- noteableId: 'noteable-id',
- designId: 'design-id',
- discussionIndex: 1,
- discussionWithOpenForm: '',
- ...props,
- },
- data() {
- return {
- ...data,
- };
- },
- mocks: {
- $apollo,
- $route: {
- hash: '#note_1',
- },
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when discussion is not resolvable', () => {
- beforeEach(() => {
- createComponent({
- discussion: {
- ...discussion,
- resolvable: false,
- },
- });
- });
-
- it('does not render an icon to resolve a thread', () => {
- expect(findResolveIcon().exists()).toBe(false);
- });
-
- it('does not render a checkbox in reply form', () => {
- findReplyPlaceholder().vm.$emit('onMouseDown');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findResolveCheckbox().exists()).toBe(false);
- });
- });
- });
-
- describe('when discussion is unresolved', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders correct amount of discussion notes', () => {
- expect(findDesignNotes()).toHaveLength(2);
- expect(findDesignNotes().wrappers.every(w => w.isVisible())).toBe(true);
- });
-
- it('renders reply placeholder', () => {
- expect(findReplyPlaceholder().isVisible()).toBe(true);
- });
-
- it('does not render toggle replies widget', () => {
- expect(findRepliesWidget().exists()).toBe(false);
- });
-
- it('renders a correct icon to resolve a thread', () => {
- expect(findResolveIcon().props('name')).toBe('check-circle');
- });
-
- it('renders a checkbox with Resolve thread text in reply form', () => {
- findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findResolveCheckbox().text()).toBe('Resolve thread');
- });
- });
-
- it('does not render resolved message', () => {
- expect(findResolvedMessage().exists()).toBe(false);
- });
- });
-
- describe('when discussion is resolved', () => {
- beforeEach(() => {
- createComponent({
- discussion: {
- ...discussion,
- resolved: true,
- resolvedBy: notes[0].author,
- resolvedAt: '2020-05-08T07:10:45Z',
- },
- });
- });
-
- it('shows only the first note', () => {
- expect(
- findDesignNotes()
- .at(0)
- .isVisible(),
- ).toBe(true);
- expect(
- findDesignNotes()
- .at(1)
- .isVisible(),
- ).toBe(false);
- });
-
- it('renders resolved message', () => {
- expect(findResolvedMessage().exists()).toBe(true);
- });
-
- it('does not show renders reply placeholder', () => {
- expect(findReplyPlaceholder().isVisible()).toBe(false);
- });
-
- it('renders toggle replies widget with correct props', () => {
- expect(findRepliesWidget().exists()).toBe(true);
- expect(findRepliesWidget().props()).toEqual({
- collapsed: true,
- replies: notes.slice(1),
- });
- });
-
- it('renders a correct icon to resolve a thread', () => {
- expect(findResolveIcon().props('name')).toBe('check-circle-filled');
- });
-
- describe('when replies are expanded', () => {
- beforeEach(() => {
- findRepliesWidget().vm.$emit('toggle');
- return wrapper.vm.$nextTick();
- });
-
- it('renders replies widget with collapsed prop equal to false', () => {
- expect(findRepliesWidget().props('collapsed')).toBe(false);
- });
-
- it('renders the second note', () => {
- expect(
- findDesignNotes()
- .at(1)
- .isVisible(),
- ).toBe(true);
- });
-
- it('renders a reply placeholder', () => {
- expect(findReplyPlaceholder().isVisible()).toBe(true);
- });
-
- it('renders a checkbox with Unresolve thread text in reply form', () => {
- findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findResolveCheckbox().text()).toBe('Unresolve thread');
- });
- });
- });
- });
-
- it('hides reply placeholder and opens form on placeholder click', () => {
- createComponent();
- findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findReplyPlaceholder().exists()).toBe(false);
- expect(findReplyForm().exists()).toBe(true);
- });
- });
-
- it('calls mutation on submitting form and closes the form', () => {
- createComponent(
- { discussionWithOpenForm: discussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
-
- findReplyForm().vm.$emit('submitForm');
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
-
- return mutate()
- .then(() => {
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findReplyForm().exists()).toBe(false);
- });
- });
-
- it('clears the discussion comment on closing comment form', () => {
- createComponent(
- { discussionWithOpenForm: discussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findReplyForm().vm.$emit('cancelForm');
-
- expect(wrapper.vm.discussionComment).toBe('');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findReplyForm().exists()).toBe(false);
- });
- });
-
- it('applies correct class to design notes when discussion is highlighted', () => {
- createComponent(
- {},
- {
- activeDiscussion: {
- id: notes[0].id,
- source: 'pin',
- },
- },
- );
-
- expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe(
- true,
- );
- });
-
- it('calls toggleResolveDiscussion mutation on resolve thread button click', () => {
- createComponent();
- findResolveButton().trigger('click');
- expect(mutate).toHaveBeenCalledWith({
- mutation: toggleResolveDiscussionMutation,
- variables: {
- id: discussion.id,
- resolve: true,
- },
- });
- return wrapper.vm.$nextTick(() => {
- expect(findResolveLoadingIcon().exists()).toBe(true);
- });
- });
-
- it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
- createComponent(
- { discussionWithOpenForm: discussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
- findResolveButton().trigger('click');
- findReplyForm().vm.$emit('submitForm');
-
- return mutate().then(() => {
- expect(mutate).toHaveBeenCalledWith({
- mutation: toggleResolveDiscussionMutation,
- variables: {
- id: discussion.id,
- resolve: true,
- },
- });
- });
- });
-
- it('emits openForm event on opening the form', () => {
- createComponent();
- findReplyPlaceholder().vm.$emit('onClick');
-
- expect(wrapper.emitted('openForm')).toBeTruthy();
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js
deleted file mode 100644
index aa187cd1388..00000000000
--- a/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
-import DesignNote from '~/design_management_legacy/components/design_notes/design_note.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue';
-
-const scrollIntoViewMock = jest.fn();
-const note = {
- id: 'gid://gitlab/DiffNote/123',
- author: {
- id: 'author-id',
- },
- body: 'test',
- userPermissions: {
- adminNote: false,
- },
-};
-HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
-
-const $route = {
- hash: '#note_123',
-};
-
-const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } });
-
-describe('Design note component', () => {
- let wrapper;
-
- const findUserAvatar = () => wrapper.find(UserAvatarLink);
- const findUserLink = () => wrapper.find('.js-user-link');
- const findReplyForm = () => wrapper.find(DesignReplyForm);
- const findEditButton = () => wrapper.find('.js-note-edit');
- const findNoteContent = () => wrapper.find('.js-note-text');
-
- function createComponent(props = {}, data = { isEditing: false }) {
- wrapper = shallowMount(DesignNote, {
- propsData: {
- note: {},
- ...props,
- },
- data() {
- return {
- ...data,
- };
- },
- mocks: {
- $route,
- $apollo: {
- mutate,
- },
- },
- stubs: {
- ApolloMutation,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should match the snapshot', () => {
- createComponent({
- note,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('should render an author', () => {
- createComponent({
- note,
- });
-
- expect(findUserAvatar().exists()).toBe(true);
- expect(findUserLink().exists()).toBe(true);
- });
-
- it('should render a time ago tooltip if note has createdAt property', () => {
- createComponent({
- note: {
- ...note,
- createdAt: '2019-07-26T15:02:20Z',
- },
- });
-
- expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
- });
-
- it('should trigger a scrollIntoView method', () => {
- createComponent({
- note,
- });
-
- expect(scrollIntoViewMock).toHaveBeenCalled();
- });
-
- it('should not render edit icon when user does not have a permission', () => {
- createComponent({
- note,
- });
-
- expect(findEditButton().exists()).toBe(false);
- });
-
- describe('when user has a permission to edit note', () => {
- it('should open an edit form on edit button click', () => {
- createComponent({
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
- },
- },
- });
-
- findEditButton().trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findReplyForm().exists()).toBe(true);
- expect(findNoteContent().exists()).toBe(false);
- });
- });
-
- describe('when edit form is rendered', () => {
- beforeEach(() => {
- createComponent(
- {
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
- },
- },
- },
- { isEditing: true },
- );
- });
-
- it('should not render note content and should render reply form', () => {
- expect(findNoteContent().exists()).toBe(false);
- expect(findReplyForm().exists()).toBe(true);
- });
-
- it('hides the form on hideForm event', () => {
- findReplyForm().vm.$emit('cancelForm');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findReplyForm().exists()).toBe(false);
- expect(findNoteContent().exists()).toBe(true);
- });
- });
-
- it('calls a mutation on submitForm event and hides a form', () => {
- findReplyForm().vm.$emit('submitForm');
- expect(mutate).toHaveBeenCalled();
-
- return mutate()
- .then(() => {
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findReplyForm().exists()).toBe(false);
- expect(findNoteContent().exists()).toBe(true);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js
deleted file mode 100644
index 088a71b64af..00000000000
--- a/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js
+++ /dev/null
@@ -1,184 +0,0 @@
-import { mount } from '@vue/test-utils';
-import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue';
-
-const showModal = jest.fn();
-
-const GlModal = {
- template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
- methods: {
- show: showModal,
- },
-};
-
-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' });
-
- function createComponent(props = {}, mountOptions = {}) {
- wrapper = mount(DesignReplyForm, {
- propsData: {
- value: '',
- isSaving: false,
- ...props,
- },
- stubs: { GlModal },
- ...mountOptions,
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('textarea has focus after component mount', () => {
- // We need to attach to document, so that `document.activeElement` is properly set in jsdom
- createComponent({}, { attachToDocument: true });
-
- expect(findTextarea().element).toEqual(document.activeElement);
- });
-
- it('renders button text as "Comment" when creating a comment', () => {
- createComponent();
-
- expect(findSubmitButton().html()).toMatchSnapshot();
- });
-
- it('renders button text as "Save comment" when creating a comment', () => {
- createComponent({ isNewComment: false });
-
- expect(findSubmitButton().html()).toMatchSnapshot();
- });
-
- describe('when form has no text', () => {
- beforeEach(() => {
- createComponent({
- value: '',
- });
- });
-
- it('submit button is disabled', () => {
- expect(findSubmitButton().attributes().disabled).toBeTruthy();
- });
-
- it('does not emit submitForm event on textarea ctrl+enter keydown', () => {
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeFalsy();
- });
- });
-
- it('does not emit submitForm event on textarea meta+enter keydown', () => {
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeFalsy();
- });
- });
-
- it('emits cancelForm event on pressing escape button on textarea', () => {
- findTextarea().trigger('keyup.esc');
-
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
- });
-
- it('emits cancelForm event on clicking Cancel button', () => {
- findCancelButton().vm.$emit('click');
-
- expect(wrapper.emitted('cancelForm')).toHaveLength(1);
- });
- });
-
- describe('when form has text', () => {
- beforeEach(() => {
- createComponent({
- value: 'test',
- });
- });
-
- it('submit button is enabled', () => {
- expect(findSubmitButton().attributes().disabled).toBeFalsy();
- });
-
- it('emits submitForm event on Comment button click', () => {
- findSubmitButton().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
- });
- });
-
- it('emits submitForm event on textarea ctrl+enter keydown', () => {
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
- });
- });
-
- it('emits submitForm event on textarea meta+enter keydown', () => {
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('submitForm')).toBeTruthy();
- });
- });
-
- it('emits input event on changing textarea content', () => {
- findTextarea().setValue('test2');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('input')).toBeTruthy();
- });
- });
-
- it('emits cancelForm event on Escape key if text was not changed', () => {
- findTextarea().trigger('keyup.esc');
-
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
- });
-
- it('opens confirmation modal on Escape key when text has changed', () => {
- wrapper.setProps({ value: 'test2' });
-
- return wrapper.vm.$nextTick().then(() => {
- findTextarea().trigger('keyup.esc');
- expect(showModal).toHaveBeenCalled();
- });
- });
-
- it('emits cancelForm event on Cancel button click if text was not changed', () => {
- findCancelButton().trigger('click');
-
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
- });
-
- it('opens confirmation modal on Cancel button click when text has changed', () => {
- wrapper.setProps({ value: 'test2' });
-
- return wrapper.vm.$nextTick().then(() => {
- findCancelButton().trigger('click');
- expect(showModal).toHaveBeenCalled();
- });
- });
-
- it('emits cancelForm event on modal Ok button click', () => {
- findTextarea().trigger('keyup.esc');
- findModal().vm.$emit('ok');
-
- expect(wrapper.emitted('cancelForm')).toBeTruthy();
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js
deleted file mode 100644
index acc7cbbca52..00000000000
--- a/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import ToggleRepliesWidget from '~/design_management_legacy/components/design_notes/toggle_replies_widget.vue';
-import notes from '../../mock_data/notes';
-
-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);
-
- function createComponent(props = {}) {
- wrapper = shallowMount(ToggleRepliesWidget, {
- propsData: {
- collapsed: true,
- replies: notes,
- ...props,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when replies are collapsed', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('should not have expanded class', () => {
- expect(findToggleWrapper().classes()).not.toContain('expanded');
- });
-
- it('should render chevron-right icon', () => {
- expect(findIcon().props('name')).toBe('chevron-right');
- });
-
- it('should have replies length on button', () => {
- expect(findButton().text()).toBe('2 replies');
- });
-
- it('should render a link to the last reply author', () => {
- expect(findAuthorLink().exists()).toBe(true);
- expect(findAuthorLink().text()).toBe(notes[1].author.name);
- expect(findAuthorLink().attributes('href')).toBe(notes[1].author.webUrl);
- });
-
- it('should render correct time ago tooltip', () => {
- expect(findTimeAgo().exists()).toBe(true);
- expect(findTimeAgo().props('time')).toBe(notes[1].createdAt);
- });
- });
-
- describe('when replies are expanded', () => {
- beforeEach(() => {
- createComponent({ collapsed: false });
- });
-
- it('should have expanded class', () => {
- expect(findToggleWrapper().classes()).toContain('expanded');
- });
-
- it('should render chevron-down icon', () => {
- expect(findIcon().props('name')).toBe('chevron-down');
- });
-
- it('should have Collapse replies text on button', () => {
- expect(findButton().text()).toBe('Collapse replies');
- });
-
- it('should not have a link to the last reply author', () => {
- expect(findAuthorLink().exists()).toBe(false);
- });
-
- it('should not render time ago tooltip', () => {
- expect(findTimeAgo().exists()).toBe(false);
- });
- });
-
- it('should emit toggle event on icon click', () => {
- createComponent();
- findIcon().vm.$emit('click', new MouseEvent('click'));
-
- expect(wrapper.emitted('toggle')).toHaveLength(1);
- });
-
- it('should emit toggle event on button click', () => {
- createComponent();
- findButton().vm.$emit('click', new MouseEvent('click'));
-
- expect(wrapper.emitted('toggle')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_overlay_spec.js b/spec/frontend/design_management_legacy/components/design_overlay_spec.js
deleted file mode 100644
index c014f3479f4..00000000000
--- a/spec/frontend/design_management_legacy/components/design_overlay_spec.js
+++ /dev/null
@@ -1,410 +0,0 @@
-import { mount } from '@vue/test-utils';
-import DesignOverlay from '~/design_management_legacy/components/design_overlay.vue';
-import updateActiveDiscussion from '~/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql';
-import notes from '../mock_data/notes';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management_legacy/constants';
-
-const mutate = jest.fn(() => Promise.resolve());
-
-describe('Design overlay component', () => {
- let wrapper;
-
- const mockDimensions = { width: 100, height: 100 };
-
- const findOverlay = () => wrapper.find('.image-diff-overlay');
- const findAllNotes = () => wrapper.findAll('.js-image-badge');
- const findCommentBadge = () => wrapper.find('.comment-indicator');
- const findFirstBadge = () => findAllNotes().at(0);
- const findSecondBadge = () => findAllNotes().at(1);
-
- const clickAndDragBadge = (elem, fromPoint, toPoint) => {
- elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
- return wrapper.vm.$nextTick().then(() => {
- elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
- return wrapper.vm.$nextTick();
- });
- };
-
- function createComponent(props = {}, data = {}) {
- wrapper = mount(DesignOverlay, {
- propsData: {
- dimensions: mockDimensions,
- position: {
- top: '0',
- left: '0',
- },
- resolvedDiscussionsExpanded: false,
- ...props,
- },
- data() {
- return {
- activeDiscussion: {
- id: null,
- source: null,
- },
- ...data,
- };
- },
- mocks: {
- $apollo: {
- mutate,
- },
- },
- });
- }
-
- it('should have correct inline style', () => {
- createComponent();
-
- expect(wrapper.find('.image-diff-overlay').attributes().style).toBe(
- 'width: 100px; height: 100px; top: 0px; left: 0px;',
- );
- });
-
- it('should emit `openCommentForm` when clicking on overlay', () => {
- createComponent();
- const newCoordinates = {
- x: 10,
- y: 10,
- };
-
- wrapper
- .find('.image-diff-overlay-add-comment')
- .trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('openCommentForm')).toEqual([
- [{ x: newCoordinates.x, y: newCoordinates.y }],
- ]);
- });
- });
-
- describe('with notes', () => {
- it('should render only the first note', () => {
- createComponent({
- notes,
- });
- expect(findAllNotes()).toHaveLength(1);
- });
-
- describe('with resolved discussions toggle expanded', () => {
- beforeEach(() => {
- createComponent({
- notes,
- resolvedDiscussionsExpanded: true,
- });
- });
-
- it('should render all notes', () => {
- expect(findAllNotes()).toHaveLength(notes.length);
- });
-
- it('should have set the correct position for each note badge', () => {
- expect(findFirstBadge().attributes().style).toBe('left: 10px; top: 15px;');
- expect(findSecondBadge().attributes().style).toBe('left: 50px; top: 50px;');
- });
-
- it('should apply resolved class to the resolved note pin', () => {
- expect(findSecondBadge().classes()).toContain('resolved');
- });
-
- it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
- wrapper.setData({
- activeDiscussion: {
- id: notes[0].id,
- source: 'discussion',
- },
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findSecondBadge().classes()).toContain('inactive');
- });
- });
- });
-
- it('should recalculate badges positions on window resize', () => {
- createComponent({
- notes,
- dimensions: {
- width: 400,
- height: 400,
- },
- });
-
- expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;');
-
- wrapper.setProps({
- dimensions: {
- width: 200,
- height: 200,
- },
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;');
- });
- });
-
- it('should call an update active discussion mutation when clicking a note without moving it', () => {
- const note = notes[0];
- const { position } = note;
- const mutationVariables = {
- mutation: updateActiveDiscussion,
- variables: {
- id: note.id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
- },
- };
-
- findFirstBadge().trigger('mousedown', { clientX: position.x, clientY: position.y });
-
- return wrapper.vm.$nextTick().then(() => {
- findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y });
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
- });
- });
- });
-
- describe('when moving notes', () => {
- it('should update badge style when note is being moved', () => {
- createComponent({
- notes,
- });
-
- const { position } = notes[0];
-
- return clickAndDragBadge(
- findFirstBadge(),
- { x: position.x, y: position.y },
- { x: 20, y: 20 },
- ).then(() => {
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px; cursor: move;');
- });
- });
-
- it('should emit `moveNote` event when note-moving action ends', () => {
- createComponent({ notes });
- const note = notes[0];
- const { position } = note;
- const newCoordinates = { x: 20, y: 20 };
-
- wrapper.setData({
- movingNoteNewPosition: {
- ...position,
- ...newCoordinates,
- },
- movingNoteStartPosition: {
- noteId: notes[0].id,
- discussionId: notes[0].discussion.id,
- ...position,
- },
- });
-
- const badge = findFirstBadge();
- return clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates)
- .then(() => {
- badge.trigger('mouseup');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted('moveNote')).toEqual([
- [
- {
- noteId: notes[0].id,
- discussionId: notes[0].discussion.id,
- coordinates: newCoordinates,
- },
- ],
- ]);
- });
- });
-
- describe('without [adminNote] permission', () => {
- const mockNoteNotAuthorised = {
- ...notes[0],
- userPermissions: {
- adminNote: false,
- },
- };
-
- const mockNoteCoordinates = {
- x: mockNoteNotAuthorised.position.x,
- y: mockNoteNotAuthorised.position.y,
- };
-
- it('should be unable to move a note', () => {
- createComponent({
- dimensions: mockDimensions,
- notes: [mockNoteNotAuthorised],
- });
-
- const badge = findAllNotes().at(0);
- return clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 }).then(() => {
- // note position should not change after a click-and-drag attempt
- expect(findFirstBadge().attributes().style).toContain(
- `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`,
- );
- });
- });
- });
- });
-
- describe('with a new form', () => {
- it('should render a new comment badge', () => {
- createComponent({
- currentCommentForm: {
- ...notes[0].position,
- },
- });
-
- expect(findCommentBadge().exists()).toBe(true);
- expect(findCommentBadge().attributes().style).toBe('left: 10px; top: 15px;');
- });
-
- describe('when moving the comment badge', () => {
- it('should update badge style to reflect new position', () => {
- const { position } = notes[0];
-
- createComponent({
- currentCommentForm: {
- ...position,
- },
- });
-
- return clickAndDragBadge(
- findCommentBadge(),
- { x: position.x, y: position.y },
- { x: 20, y: 20 },
- ).then(() => {
- expect(findCommentBadge().attributes().style).toBe(
- 'left: 20px; top: 20px; cursor: move;',
- );
- });
- });
-
- it('should update badge style when note-moving action ends', () => {
- const { position } = notes[0];
- createComponent({
- currentCommentForm: {
- ...position,
- },
- });
-
- const commentBadge = findCommentBadge();
- const toPoint = { x: 20, y: 20 };
-
- return clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint)
- .then(() => {
- commentBadge.trigger('mouseup');
- // simulates the currentCommentForm being updated in index.vue component, and
- // propagated back down to this prop
- wrapper.setProps({
- currentCommentForm: { height: position.height, width: position.width, ...toPoint },
- });
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;');
- });
- });
-
- it.each`
- element | getElementFunc | event
- ${'overlay'} | ${findOverlay} | ${'mouseleave'}
- ${'comment badge'} | ${findCommentBadge} | ${'mouseup'}
- `(
- 'should emit `openCommentForm` event when $event fired on $element element',
- ({ getElementFunc, event }) => {
- createComponent({
- notes,
- currentCommentForm: {
- ...notes[0].position,
- },
- });
-
- const newCoordinates = { x: 20, y: 20 };
- wrapper.setData({
- movingNoteStartPosition: {
- ...notes[0].position,
- },
- movingNoteNewPosition: {
- ...notes[0].position,
- ...newCoordinates,
- },
- });
-
- getElementFunc().trigger(event);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
- });
- },
- );
- });
- });
-
- describe('getMovingNotePositionDelta', () => {
- it('should calculate delta correctly from state', () => {
- createComponent();
-
- wrapper.setData({
- movingNoteStartPosition: {
- clientX: 10,
- clientY: 20,
- },
- });
-
- const mockMouseEvent = {
- clientX: 30,
- clientY: 10,
- };
-
- expect(wrapper.vm.getMovingNotePositionDelta(mockMouseEvent)).toEqual({
- deltaX: 20,
- deltaY: -10,
- });
- });
- });
-
- describe('isPositionInOverlay', () => {
- createComponent({ dimensions: mockDimensions });
-
- it.each`
- test | coordinates | expectedResult
- ${'within overlay bounds'} | ${{ x: 50, y: 50 }} | ${true}
- ${'outside overlay bounds'} | ${{ x: 101, y: 101 }} | ${false}
- `('returns [$expectedResult] when position is $test', ({ coordinates, expectedResult }) => {
- const position = { ...mockDimensions, ...coordinates };
-
- expect(wrapper.vm.isPositionInOverlay(position)).toBe(expectedResult);
- });
- });
-
- describe('getNoteRelativePosition', () => {
- it('calculates position correctly', () => {
- createComponent({ dimensions: mockDimensions });
- const position = { x: 50, y: 50, width: 200, height: 200 };
-
- expect(wrapper.vm.getNoteRelativePosition(position)).toEqual({ left: 25, top: 25 });
- });
- });
-
- describe('canMoveNote', () => {
- it.each`
- adminNotePermission | canMoveNoteResult
- ${true} | ${true}
- ${false} | ${false}
- ${undefined} | ${false}
- `(
- 'returns [$canMoveNoteResult] when [adminNote permission] is [$adminNotePermission]',
- ({ adminNotePermission, canMoveNoteResult }) => {
- createComponent();
-
- const note = {
- userPermissions: {
- adminNote: adminNotePermission,
- },
- };
- expect(wrapper.vm.canMoveNote(note)).toBe(canMoveNoteResult);
- },
- );
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_presentation_spec.js b/spec/frontend/design_management_legacy/components/design_presentation_spec.js
deleted file mode 100644
index ceff86b0549..00000000000
--- a/spec/frontend/design_management_legacy/components/design_presentation_spec.js
+++ /dev/null
@@ -1,553 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import DesignPresentation from '~/design_management_legacy/components/design_presentation.vue';
-import DesignOverlay from '~/design_management_legacy/components/design_overlay.vue';
-
-const mockOverlayData = {
- overlayDimensions: {
- width: 100,
- height: 100,
- },
- overlayPosition: {
- top: '0',
- left: '0',
- },
-};
-
-describe('Design management design presentation component', () => {
- let wrapper;
-
- function createComponent(
- {
- image,
- imageName,
- discussions = [],
- isAnnotating = false,
- resolvedDiscussionsExpanded = false,
- } = {},
- data = {},
- stubs = {},
- ) {
- wrapper = shallowMount(DesignPresentation, {
- propsData: {
- image,
- imageName,
- discussions,
- isAnnotating,
- resolvedDiscussionsExpanded,
- },
- stubs,
- });
-
- wrapper.setData(data);
- wrapper.element.scrollTo = jest.fn();
- }
-
- const findOverlayCommentButton = () => wrapper.find('.image-diff-overlay-add-comment');
-
- /**
- * Spy on $refs and mock given values
- * @param {Object} viewportDimensions {width, height}
- * @param {Object} childDimensions {width, height}
- * @param {Float} scrollTopPerc 0 < x < 1
- * @param {Float} scrollLeftPerc 0 < x < 1
- */
- function mockRefDimensions(
- ref,
- viewportDimensions,
- childDimensions,
- scrollTopPerc,
- scrollLeftPerc,
- ) {
- jest.spyOn(ref, 'scrollWidth', 'get').mockReturnValue(childDimensions.width);
- jest.spyOn(ref, 'scrollHeight', 'get').mockReturnValue(childDimensions.height);
- jest.spyOn(ref, 'offsetWidth', 'get').mockReturnValue(viewportDimensions.width);
- jest.spyOn(ref, 'offsetHeight', 'get').mockReturnValue(viewportDimensions.height);
- jest
- .spyOn(ref, 'scrollLeft', 'get')
- .mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc);
- jest
- .spyOn(ref, 'scrollTop', 'get')
- .mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc);
- }
-
- function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) {
- const event = useTouchEvents
- ? {
- mousedown: 'touchstart',
- mousemove: 'touchmove',
- mouseup: 'touchend',
- }
- : {
- mousedown: 'mousedown',
- mousemove: 'mousemove',
- mouseup: 'mouseup',
- };
-
- const addCommentOverlay = findOverlayCommentButton();
-
- // triggering mouse events on this element best simulates
- // reality, as it is the lowest-level node that needs to
- // respond to mouse events
- addCommentOverlay.trigger(event.mousedown, {
- clientX: startCoords.clientX,
- clientY: startCoords.clientY,
- });
- return wrapper.vm
- .$nextTick()
- .then(() => {
- addCommentOverlay.trigger(event.mousemove, {
- clientX: endCoords.clientX,
- clientY: endCoords.clientY,
- });
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- if (mouseup) {
- addCommentOverlay.trigger(event.mouseup);
- return wrapper.vm.$nextTick();
- }
-
- return undefined;
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders image and overlay when image provided', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- mockOverlayData,
- );
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders empty state when no image provided', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('openCommentForm event emits correct data', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- mockOverlayData,
- );
-
- wrapper.vm.openCommentForm({ x: 1, y: 1 });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('openCommentForm')).toEqual([
- [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }],
- ]);
- });
- });
-
- describe('currentCommentForm', () => {
- it('is null when isAnnotating is false', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- mockOverlayData,
- );
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.currentCommentForm).toBeNull();
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('is null when isAnnotating is true but annotation position is falsey', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- isAnnotating: true,
- },
- mockOverlayData,
- );
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.currentCommentForm).toBeNull();
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('is equal to current annotation position when isAnnotating is true', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- isAnnotating: true,
- },
- {
- ...mockOverlayData,
- currentAnnotationPosition: {
- x: 1,
- y: 1,
- width: 100,
- height: 100,
- },
- },
- );
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.currentCommentForm).toEqual({
- x: 1,
- y: 1,
- width: 100,
- height: 100,
- });
- expect(wrapper.element).toMatchSnapshot();
- });
- });
- });
-
- describe('setOverlayPosition', () => {
- beforeEach(() => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- mockOverlayData,
- );
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it('sets overlay position correctly when overlay is smaller than viewport', () => {
- jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
- jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
-
- wrapper.vm.setOverlayPosition();
- expect(wrapper.vm.overlayPosition).toEqual({
- left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
- top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
- });
- });
-
- it('sets overlay position correctly when overlay width is larger than viewports', () => {
- jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(50);
- jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
-
- wrapper.vm.setOverlayPosition();
- expect(wrapper.vm.overlayPosition).toEqual({
- left: '0',
- top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
- });
- });
-
- it('sets overlay position correctly when overlay height is larger than viewports', () => {
- jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
- jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(50);
-
- wrapper.vm.setOverlayPosition();
- expect(wrapper.vm.overlayPosition).toEqual({
- left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
- top: '0',
- });
- });
- });
-
- describe('getViewportCenter', () => {
- beforeEach(() => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- mockOverlayData,
- );
- });
-
- it('calculate center correctly with no scroll', () => {
- mockRefDimensions(
- wrapper.vm.$refs.presentationViewport,
- { width: 10, height: 10 },
- { width: 20, height: 20 },
- 0,
- 0,
- );
-
- expect(wrapper.vm.getViewportCenter()).toEqual({
- x: 5,
- y: 5,
- });
- });
-
- it('calculate center correctly with some scroll', () => {
- mockRefDimensions(
- wrapper.vm.$refs.presentationViewport,
- { width: 10, height: 10 },
- { width: 20, height: 20 },
- 0.5,
- 0.5,
- );
-
- expect(wrapper.vm.getViewportCenter()).toEqual({
- x: 10,
- y: 10,
- });
- });
-
- it('Returns default case if no overflow (scrollWidth==offsetWidth, etc.)', () => {
- mockRefDimensions(
- wrapper.vm.$refs.presentationViewport,
- { width: 20, height: 20 },
- { width: 20, height: 20 },
- 0.5,
- 0.5,
- );
-
- expect(wrapper.vm.getViewportCenter()).toEqual({
- x: 10,
- y: 10,
- });
- });
- });
-
- describe('scaleZoomFocalPoint', () => {
- it('scales focal point correctly when zooming in', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- {
- ...mockOverlayData,
- zoomFocalPoint: {
- x: 5,
- y: 5,
- width: 50,
- height: 50,
- },
- },
- );
-
- wrapper.vm.scaleZoomFocalPoint();
- expect(wrapper.vm.zoomFocalPoint).toEqual({
- x: 10,
- y: 10,
- width: 100,
- height: 100,
- });
- });
-
- it('scales focal point correctly when zooming out', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- {
- ...mockOverlayData,
- zoomFocalPoint: {
- x: 10,
- y: 10,
- width: 200,
- height: 200,
- },
- },
- );
-
- wrapper.vm.scaleZoomFocalPoint();
- expect(wrapper.vm.zoomFocalPoint).toEqual({
- x: 5,
- y: 5,
- width: 100,
- height: 100,
- });
- });
- });
-
- describe('onImageResize', () => {
- it('sets zoom focal point on initial load', () => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- mockOverlayData,
- );
-
- wrapper.setMethods({
- shiftZoomFocalPoint: jest.fn(),
- scaleZoomFocalPoint: jest.fn(),
- scrollToFocalPoint: jest.fn(),
- });
-
- wrapper.vm.onImageResize({ width: 10, height: 10 });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.shiftZoomFocalPoint).toHaveBeenCalled();
- expect(wrapper.vm.initialLoad).toBe(false);
- });
- });
-
- it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', () => {
- wrapper.vm.onImageResize({ width: 10, height: 10 });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled();
- expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled();
- });
- });
- });
-
- describe('onPresentationMousedown', () => {
- it.each`
- scenario | width | height
- ${'width overflows'} | ${101} | ${100}
- ${'height overflows'} | ${100} | ${101}
- ${'width and height overflows'} | ${200} | ${200}
- `('sets lastDragPosition when design $scenario', ({ width, height }) => {
- createComponent();
- mockRefDimensions(
- wrapper.vm.$refs.presentationViewport,
- { width: 100, height: 100 },
- { width, height },
- );
-
- const newLastDragPosition = { x: 2, y: 2 };
- wrapper.vm.onPresentationMousedown({
- clientX: newLastDragPosition.x,
- clientY: newLastDragPosition.y,
- });
-
- expect(wrapper.vm.lastDragPosition).toStrictEqual(newLastDragPosition);
- });
-
- it('does not set lastDragPosition if design does not overflow', () => {
- const lastDragPosition = { x: 1, y: 1 };
-
- createComponent({}, { lastDragPosition });
- mockRefDimensions(
- wrapper.vm.$refs.presentationViewport,
- { width: 100, height: 100 },
- { width: 50, height: 50 },
- );
-
- wrapper.vm.onPresentationMousedown({ clientX: 2, clientY: 2 });
-
- // check lastDragPosition is unchanged
- expect(wrapper.vm.lastDragPosition).toStrictEqual(lastDragPosition);
- });
- });
-
- describe('getAnnotationPositon', () => {
- it.each`
- coordinates | overlayDimensions | position
- ${{ x: 100, y: 100 }} | ${{ width: 50, height: 50 }} | ${{ x: 100, y: 100, width: 50, height: 50 }}
- ${{ x: 100.2, y: 100.5 }} | ${{ width: 50.6, height: 50.0 }} | ${{ x: 100, y: 101, width: 51, height: 50 }}
- `('returns correct annotation position', ({ coordinates, overlayDimensions, position }) => {
- createComponent(undefined, {
- overlayDimensions: {
- width: overlayDimensions.width,
- height: overlayDimensions.height,
- },
- });
-
- expect(wrapper.vm.getAnnotationPositon(coordinates)).toStrictEqual(position);
- });
- });
-
- describe('when design is overflowing', () => {
- beforeEach(() => {
- createComponent(
- {
- image: 'test.jpg',
- imageName: 'test',
- },
- mockOverlayData,
- {
- 'design-overlay': DesignOverlay,
- },
- );
-
- // mock a design that overflows
- mockRefDimensions(
- wrapper.vm.$refs.presentationViewport,
- { width: 10, height: 10 },
- { width: 20, height: 20 },
- 0,
- 0,
- );
- });
-
- it('opens a comment form if design was not dragged', () => {
- const addCommentOverlay = findOverlayCommentButton();
- const startCoords = {
- clientX: 1,
- clientY: 1,
- };
-
- addCommentOverlay.trigger('mousedown', {
- clientX: startCoords.clientX,
- clientY: startCoords.clientY,
- });
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- addCommentOverlay.trigger('mouseup');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted('openCommentForm')).toBeDefined();
- });
- });
-
- describe('when clicking and dragging', () => {
- it.each`
- description | useTouchEvents
- ${'with touch events'} | ${true}
- ${'without touch events'} | ${false}
- `('calls scrollTo with correct arguments $description', ({ useTouchEvents }) => {
- return clickDragExplore(
- { clientX: 0, clientY: 0 },
- { clientX: 10, clientY: 10 },
- { useTouchEvents },
- ).then(() => {
- expect(wrapper.element.scrollTo).toHaveBeenCalledTimes(1);
- expect(wrapper.element.scrollTo).toHaveBeenCalledWith(-10, -10);
- });
- });
-
- it('does not open a comment form when drag position exceeds buffer', () => {
- return clickDragExplore(
- { clientX: 0, clientY: 0 },
- { clientX: 10, clientY: 10 },
- { mouseup: true },
- ).then(() => {
- expect(wrapper.emitted('openCommentForm')).toBeFalsy();
- });
- });
-
- it('opens a comment form when drag position is within buffer', () => {
- return clickDragExplore(
- { clientX: 0, clientY: 0 },
- { clientX: 1, clientY: 0 },
- { mouseup: true },
- ).then(() => {
- expect(wrapper.emitted('openCommentForm')).toBeDefined();
- });
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_scaler_spec.js b/spec/frontend/design_management_legacy/components/design_scaler_spec.js
deleted file mode 100644
index 30ef5ab159b..00000000000
--- a/spec/frontend/design_management_legacy/components/design_scaler_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import DesignScaler from '~/design_management_legacy/components/design_scaler.vue';
-
-describe('Design management design scaler component', () => {
- let wrapper;
-
- function createComponent(propsData, data = {}) {
- wrapper = shallowMount(DesignScaler, {
- propsData,
- });
- wrapper.setData(data);
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const getButton = type => {
- const buttonTypeOrder = ['minus', 'reset', 'plus'];
- const buttons = wrapper.findAll('button');
- return buttons.at(buttonTypeOrder.indexOf(type));
- };
-
- it('emits @scale event when "plus" button clicked', () => {
- createComponent();
-
- getButton('plus').trigger('click');
- expect(wrapper.emitted('scale')).toEqual([[1.2]]);
- });
-
- it('emits @scale event when "reset" button clicked (scale > 1)', () => {
- createComponent({}, { scale: 1.6 });
- return wrapper.vm.$nextTick().then(() => {
- getButton('reset').trigger('click');
- expect(wrapper.emitted('scale')).toEqual([[1]]);
- });
- });
-
- it('emits @scale event when "minus" button clicked (scale > 1)', () => {
- createComponent({}, { scale: 1.6 });
-
- return wrapper.vm.$nextTick().then(() => {
- getButton('minus').trigger('click');
- expect(wrapper.emitted('scale')).toEqual([[1.4]]);
- });
- });
-
- it('minus and reset buttons are disabled when scale === 1', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('minus and reset buttons are enabled when scale > 1', () => {
- createComponent({}, { scale: 1.2 });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('plus button is disabled when scale === 2', () => {
- createComponent({}, { scale: 2 });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/design_sidebar_spec.js b/spec/frontend/design_management_legacy/components/design_sidebar_spec.js
deleted file mode 100644
index fc0f618c359..00000000000
--- a/spec/frontend/design_management_legacy/components/design_sidebar_spec.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlCollapse, GlPopover } from '@gitlab/ui';
-import Cookies from 'js-cookie';
-import DesignSidebar from '~/design_management_legacy/components/design_sidebar.vue';
-import Participants from '~/sidebar/components/participants/participants.vue';
-import DesignDiscussion from '~/design_management_legacy/components/design_notes/design_discussion.vue';
-import design from '../mock_data/design';
-import updateActiveDiscussionMutation from '~/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql';
-
-const updateActiveDiscussionMutationVariables = {
- mutation: updateActiveDiscussionMutation,
- variables: {
- id: design.discussions.nodes[0].notes.nodes[0].id,
- source: 'discussion',
- },
-};
-
-const $route = {
- params: {
- id: '1',
- },
-};
-
-const cookieKey = 'hide_design_resolved_comments_popover';
-
-const mutate = jest.fn().mockResolvedValue();
-
-describe('Design management design sidebar component', () => {
- let wrapper;
-
- const findDiscussions = () => wrapper.findAll(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 findCollapsible = () => wrapper.find(GlCollapse);
- const findToggleResolvedCommentsButton = () => wrapper.find('[data-testid="resolved-comments"]');
- const findPopover = () => wrapper.find(GlPopover);
- const findNewDiscussionDisclaimer = () =>
- wrapper.find('[data-testid="new-discussion-disclaimer"]');
-
- function createComponent(props = {}) {
- wrapper = shallowMount(DesignSidebar, {
- propsData: {
- design,
- resolvedDiscussionsExpanded: false,
- markdownPreviewPath: '',
- ...props,
- },
- mocks: {
- $route,
- $apollo: {
- mutate,
- },
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders participants', () => {
- createComponent();
-
- expect(findParticipants().exists()).toBe(true);
- });
-
- it('passes the correct amount of participants to the Participants component', () => {
- createComponent();
-
- expect(findParticipants().props('participants')).toHaveLength(1);
- });
-
- describe('when has no discussions', () => {
- beforeEach(() => {
- createComponent({
- design: {
- ...design,
- discussions: {
- nodes: [],
- },
- },
- });
- });
-
- it('does not render discussions', () => {
- expect(findDiscussions().exists()).toBe(false);
- });
-
- it('renders a message about possibility to create a new discussion', () => {
- expect(findNewDiscussionDisclaimer().exists()).toBe(true);
- });
- });
-
- describe('when has discussions', () => {
- beforeEach(() => {
- Cookies.set(cookieKey, true);
- createComponent();
- });
-
- it('renders correct amount of unresolved discussions', () => {
- expect(findUnresolvedDiscussions()).toHaveLength(1);
- });
-
- it('renders correct amount of resolved discussions', () => {
- expect(findResolvedDiscussions()).toHaveLength(1);
- });
-
- it('has resolved comments collapsible collapsed', () => {
- expect(findCollapsible().attributes('visible')).toBeUndefined();
- });
-
- it('emits toggleResolveComments event on resolve comments button click', () => {
- findToggleResolvedCommentsButton().vm.$emit('click');
- expect(wrapper.emitted('toggleResolvedComments')).toHaveLength(1);
- });
-
- it('opens a collapsible when resolvedDiscussionsExpanded prop changes to true', () => {
- expect(findCollapsible().attributes('visible')).toBeUndefined();
- wrapper.setProps({
- resolvedDiscussionsExpanded: true,
- });
- return wrapper.vm.$nextTick().then(() => {
- expect(findCollapsible().attributes('visible')).toBe('true');
- });
- });
-
- it('does not popover about resolved comments', () => {
- expect(findPopover().exists()).toBe(false);
- });
-
- it('sends a mutation to set an active discussion when clicking on a discussion', () => {
- findFirstDiscussion().trigger('click');
-
- expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables);
- });
-
- it('sends a mutation to reset an active discussion when clicking outside of discussion', () => {
- wrapper.trigger('click');
-
- expect(mutate).toHaveBeenCalledWith({
- ...updateActiveDiscussionMutationVariables,
- variables: { id: undefined, source: 'discussion' },
- });
- });
-
- it('emits correct event on discussion create note error', () => {
- findFirstDiscussion().vm.$emit('createNoteError', 'payload');
- expect(wrapper.emitted('onDesignDiscussionError')).toEqual([['payload']]);
- });
-
- it('emits correct event on discussion update note error', () => {
- findFirstDiscussion().vm.$emit('updateNoteError', 'payload');
- expect(wrapper.emitted('updateNoteError')).toEqual([['payload']]);
- });
-
- it('emits correct event on discussion resolve error', () => {
- findFirstDiscussion().vm.$emit('resolveDiscussionError', 'payload');
- expect(wrapper.emitted('resolveDiscussionError')).toEqual([['payload']]);
- });
-
- it('changes prop correctly on opening discussion form', () => {
- findFirstDiscussion().vm.$emit('openForm', 'some-id');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findFirstDiscussion().props('discussionWithOpenForm')).toBe('some-id');
- });
- });
- });
-
- describe('when all discussions are resolved', () => {
- beforeEach(() => {
- createComponent({
- design: {
- ...design,
- discussions: {
- nodes: [
- {
- id: 'discussion-id',
- replyId: 'discussion-reply-id',
- resolved: true,
- notes: {
- nodes: [
- {
- id: 'note-id',
- body: '123',
- author: {
- name: 'Administrator',
- username: 'root',
- webUrl: 'link-to-author',
- avatarUrl: 'link-to-avatar',
- },
- },
- ],
- },
- },
- ],
- },
- },
- });
- });
-
- it('renders a message about possibility to create a new discussion', () => {
- expect(findNewDiscussionDisclaimer().exists()).toBe(true);
- });
-
- it('does not render unresolved discussions', () => {
- expect(findUnresolvedDiscussions()).toHaveLength(0);
- });
- });
-
- describe('when showing resolved discussions for the first time', () => {
- beforeEach(() => {
- Cookies.set(cookieKey, false);
- createComponent();
- });
-
- it('renders a popover if we show resolved comments collapsible for the first time', () => {
- expect(findPopover().exists()).toBe(true);
- });
-
- it('dismisses a popover on the outside click', () => {
- wrapper.trigger('click');
- return wrapper.vm.$nextTick(() => {
- expect(findPopover().exists()).toBe(false);
- });
- });
-
- it(`sets a ${cookieKey} cookie on clicking outside the popover`, () => {
- jest.spyOn(Cookies, 'set');
- wrapper.trigger('click');
- expect(Cookies.set).toHaveBeenCalledWith(cookieKey, 'true', { expires: 365 * 10 });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/image_spec.js b/spec/frontend/design_management_legacy/components/image_spec.js
deleted file mode 100644
index 265c91abb4e..00000000000
--- a/spec/frontend/design_management_legacy/components/image_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlIcon } from '@gitlab/ui';
-import DesignImage from '~/design_management_legacy/components/image.vue';
-
-describe('Design management large image component', () => {
- let wrapper;
-
- function createComponent(propsData, data = {}) {
- wrapper = shallowMount(DesignImage, {
- propsData,
- });
- wrapper.setData(data);
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders loading state', () => {
- createComponent({
- isLoading: true,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders image', () => {
- createComponent({
- isLoading: false,
- image: 'test.jpg',
- name: 'test',
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('sets correct classes and styles if imageStyle is set', () => {
- createComponent(
- {
- isLoading: false,
- image: 'test.jpg',
- name: 'test',
- },
- {
- imageStyle: {
- width: '100px',
- height: '100px',
- },
- },
- );
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders media broken icon on error', () => {
- createComponent({
- isLoading: false,
- image: 'test.jpg',
- name: 'test',
- });
-
- const image = wrapper.find('img');
- image.trigger('error');
- return wrapper.vm.$nextTick().then(() => {
- expect(image.isVisible()).toBe(false);
- expect(wrapper.find(GlIcon).element).toMatchSnapshot();
- });
- });
-
- describe('zoom', () => {
- const baseImageWidth = 100;
- const baseImageHeight = 100;
-
- beforeEach(() => {
- createComponent(
- {
- isLoading: false,
- image: 'test.jpg',
- name: 'test',
- },
- {
- imageStyle: {
- width: `${baseImageWidth}px`,
- height: `${baseImageHeight}px`,
- },
- baseImageSize: {
- width: baseImageWidth,
- height: baseImageHeight,
- },
- },
- );
-
- jest.spyOn(wrapper.vm.$refs.contentImg, 'offsetWidth', 'get').mockReturnValue(baseImageWidth);
- jest
- .spyOn(wrapper.vm.$refs.contentImg, 'offsetHeight', 'get')
- .mockReturnValue(baseImageHeight);
- });
-
- it('emits @resize event on zoom', () => {
- const zoomAmount = 2;
- wrapper.vm.zoom(zoomAmount);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('resize')).toEqual([
- [{ width: baseImageWidth * zoomAmount, height: baseImageHeight * zoomAmount }],
- ]);
- });
- });
-
- it('emits @resize event with base image size when scale=1', () => {
- wrapper.vm.zoom(1);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('resize')).toEqual([
- [{ width: baseImageWidth, height: baseImageHeight }],
- ]);
- });
- });
-
- it('sets image style when zoomed', () => {
- const zoomAmount = 2;
- wrapper.vm.zoom(zoomAmount);
- expect(wrapper.vm.imageStyle).toEqual({
- width: `${baseImageWidth * zoomAmount}px`,
- height: `${baseImageHeight * zoomAmount}px`,
- });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap
deleted file mode 100644
index 168b9424006..00000000000
--- a/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap
+++ /dev/null
@@ -1,149 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management list item component when item appears in view after image is loaded renders media broken icon when image onerror triggered 1`] = `
-<gl-icon-stub
- class="text-secondary"
- name="media-broken"
- size="32"
-/>
-`;
-
-exports[`Design management list item component with notes renders item with multiple comments 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub>
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <div
- class="ml-auto d-flex align-items-center text-secondary"
- >
- <icon-stub
- class="ml-1"
- name="comments"
- size="16"
- />
-
- <span
- aria-label="2 comments"
- class="ml-1"
- >
-
- 2
-
- </span>
- </div>
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with notes renders item with single comment 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub>
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <div
- class="ml-auto d-flex align-items-center text-secondary"
- >
- <icon-stub
- class="ml-1"
- name="comments"
- size="16"
- />
-
- <span
- aria-label="1 comment"
- class="ml-1"
- >
-
- 1
-
- </span>
- </div>
- </div>
-</router-link-stub>
-`;
diff --git a/spec/frontend/design_management_legacy/components/list/item_spec.js b/spec/frontend/design_management_legacy/components/list/item_spec.js
deleted file mode 100644
index e9bb0fc3f29..00000000000
--- a/spec/frontend/design_management_legacy/components/list/item_spec.js
+++ /dev/null
@@ -1,169 +0,0 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
-import VueRouter from 'vue-router';
-import Icon from '~/vue_shared/components/icon.vue';
-import Item from '~/design_management_legacy/components/list/item.vue';
-
-const localVue = createLocalVue();
-localVue.use(VueRouter);
-const router = new VueRouter();
-
-// Referenced from: doc/api/graphql/reference/gitlab_schema.graphql:DesignVersionEvent
-const DESIGN_VERSION_EVENT = {
- CREATION: 'CREATION',
- DELETION: 'DELETION',
- MODIFICATION: 'MODIFICATION',
- NO_CHANGE: 'NONE',
-};
-
-describe('Design management list item component', () => {
- let wrapper;
-
- const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]');
- const findEventIcon = () => findDesignEvent().find(Icon);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
-
- function createComponent({
- notesCount = 0,
- event = DESIGN_VERSION_EVENT.NO_CHANGE,
- isUploading = false,
- imageLoading = false,
- } = {}) {
- wrapper = shallowMount(Item, {
- localVue,
- router,
- propsData: {
- id: 1,
- filename: 'test',
- image: 'http://via.placeholder.com/300',
- isUploading,
- event,
- notesCount,
- updatedAt: '01-01-2019',
- },
- data() {
- return {
- imageLoading,
- };
- },
- stubs: ['router-link'],
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when item is not in view', () => {
- it('image is not rendered', () => {
- createComponent();
-
- const image = wrapper.find('img');
- expect(image.attributes('src')).toBe('');
- });
- });
-
- describe('when item appears in view', () => {
- let image;
- let glIntersectionObserver;
-
- beforeEach(() => {
- createComponent();
- image = wrapper.find('img');
- glIntersectionObserver = wrapper.find(GlIntersectionObserver);
-
- glIntersectionObserver.vm.$emit('appear');
- return wrapper.vm.$nextTick();
- });
-
- describe('before image is loaded', () => {
- it('renders loading spinner', () => {
- expect(wrapper.find(GlLoadingIcon)).toExist();
- });
- });
-
- describe('after image is loaded', () => {
- beforeEach(() => {
- image.trigger('load');
- return wrapper.vm.$nextTick();
- });
-
- it('renders an image', () => {
- expect(image.attributes('src')).toBe('http://via.placeholder.com/300');
- expect(image.isVisible()).toBe(true);
- });
-
- it('renders media broken icon when image onerror triggered', () => {
- image.trigger('error');
- return wrapper.vm.$nextTick().then(() => {
- expect(image.isVisible()).toBe(false);
- expect(wrapper.find(GlIcon).element).toMatchSnapshot();
- });
- });
-
- describe('when imageV432x230 and image provided', () => {
- it('renders imageV432x230 image', () => {
- const mockSrc = 'mock-imageV432x230-url';
- wrapper.setProps({ imageV432x230: mockSrc });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(image.attributes('src')).toBe(mockSrc);
- });
- });
- });
-
- describe('when image disappears from view and then reappears', () => {
- beforeEach(() => {
- glIntersectionObserver.vm.$emit('appear');
- return wrapper.vm.$nextTick();
- });
-
- it('renders an image', () => {
- expect(image.isVisible()).toBe(true);
- });
- });
- });
- });
-
- describe('with notes', () => {
- it('renders item with single comment', () => {
- createComponent({ notesCount: 1 });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders item with multiple comments', () => {
- createComponent({ notesCount: 2 });
-
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders loading spinner when isUploading is true', () => {
- createComponent({ isUploading: true });
-
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('renders item with no status icon for none event', () => {
- createComponent();
-
- expect(findDesignEvent().exists()).toBe(false);
- });
-
- describe('with associated event', () => {
- it.each`
- event | icon | className
- ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'text-primary-500'}
- ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'text-danger-500'}
- ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'text-success-500'}
- `('renders item with correct status icon for $event event', ({ event, icon, className }) => {
- createComponent({ event });
- const eventIcon = findEventIcon();
-
- expect(eventIcon.exists()).toBe(true);
- expect(eventIcon.props('name')).toBe(icon);
- expect(eventIcon.classes()).toContain(className);
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap
deleted file mode 100644
index e55cff8de3d..00000000000
--- a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap
+++ /dev/null
@@ -1,61 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management toolbar component renders design and updated data 1`] = `
-<header
- class="d-flex p-2 bg-white align-items-center js-design-header"
->
- <a
- aria-label="Go back to designs"
- class="mr-3 text-plain d-flex justify-content-center align-items-center"
- >
- <icon-stub
- name="close"
- size="18"
- />
- </a>
-
- <div
- class="overflow-hidden d-flex align-items-center"
- >
- <h2
- class="m-0 str-truncated-100 gl-font-base"
- >
- test.jpg
- </h2>
-
- <small
- class="text-secondary"
- >
- Updated 1 hour ago by Test Name
- </small>
- </div>
-
- <pagination-stub
- class="ml-auto flex-shrink-0"
- id="1"
- />
-
- <gl-deprecated-button-stub
- class="mr-2"
- href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d"
- size="md"
- variant="secondary"
- >
- <icon-stub
- name="download"
- size="18"
- />
- </gl-deprecated-button-stub>
-
- <delete-button-stub
- buttonclass=""
- buttonvariant="danger"
- hasselecteddesigns="true"
- >
- <icon-stub
- name="remove"
- size="18"
- />
- </delete-button-stub>
-</header>
-`;
diff --git a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap
deleted file mode 100644
index 08662a04f15..00000000000
--- a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap
+++ /dev/null
@@ -1,28 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management pagination button component disables button when no design is passed 1`] = `
-<router-link-stub
- aria-label="Test title"
- class="btn btn-default disabled"
- disabled="true"
- to="[object Object]"
->
- <icon-stub
- name="angle-right"
- size="16"
- />
-</router-link-stub>
-`;
-
-exports[`Design management pagination button component renders router-link 1`] = `
-<router-link-stub
- aria-label="Test title"
- class="btn btn-default"
- to="[object Object]"
->
- <icon-stub
- name="angle-right"
- size="16"
- />
-</router-link-stub>
-`;
diff --git a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap
deleted file mode 100644
index 0197b4bff79..00000000000
--- a/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap
+++ /dev/null
@@ -1,29 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management pagination component hides components when designs are empty 1`] = `<!---->`;
-
-exports[`Design management pagination component renders pagination buttons 1`] = `
-<div
- class="d-flex align-items-center"
->
-
- 0 of 2
-
- <div
- class="btn-group ml-3 mr-3"
- >
- <pagination-button-stub
- class="js-previous-design"
- iconname="angle-left"
- title="Go to previous design"
- />
-
- <pagination-button-stub
- class="js-next-design"
- design="[object Object]"
- iconname="angle-right"
- title="Go to next design"
- />
- </div>
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/components/toolbar/index_spec.js b/spec/frontend/design_management_legacy/components/toolbar/index_spec.js
deleted file mode 100644
index 8207cad4136..00000000000
--- a/spec/frontend/design_management_legacy/components/toolbar/index_spec.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import VueRouter from 'vue-router';
-import { GlDeprecatedButton } from '@gitlab/ui';
-import Toolbar from '~/design_management_legacy/components/toolbar/index.vue';
-import DeleteButton from '~/design_management_legacy/components/delete_button.vue';
-import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants';
-
-const localVue = createLocalVue();
-localVue.use(VueRouter);
-const router = new VueRouter();
-
-const RouterLinkStub = {
- props: {
- to: {
- type: Object,
- },
- },
- render(createElement) {
- return createElement('a', {}, this.$slots.default);
- },
-};
-
-describe('Design management toolbar component', () => {
- let wrapper;
-
- function createComponent(isLoading = false, createDesign = true, props) {
- const updatedAt = new Date();
- updatedAt.setHours(updatedAt.getHours() - 1);
-
- wrapper = shallowMount(Toolbar, {
- localVue,
- router,
- propsData: {
- id: '1',
- isLatestVersion: true,
- isLoading,
- isDeleting: false,
- filename: 'test.jpg',
- updatedAt: updatedAt.toString(),
- updatedBy: {
- name: 'Test Name',
- },
- image: '/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
- ...props,
- },
- stubs: {
- 'router-link': RouterLinkStub,
- },
- });
-
- wrapper.setData({
- permissions: {
- createDesign,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders design and updated data', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('links back to designs list', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- const link = wrapper.find('a');
-
- expect(link.props('to')).toEqual({
- name: DESIGNS_ROUTE_NAME,
- query: {
- version: undefined,
- },
- });
- });
- });
-
- it('renders delete button on latest designs version with logged in user', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DeleteButton).exists()).toBe(true);
- });
- });
-
- it('does not render delete button on non-latest version', () => {
- createComponent(false, true, { isLatestVersion: false });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DeleteButton).exists()).toBe(false);
- });
- });
-
- it('does not render delete button when user is not logged in', () => {
- createComponent(false, false);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DeleteButton).exists()).toBe(false);
- });
- });
-
- it('emits `delete` event on deleteButton `deleteSelectedDesigns` event', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- wrapper.find(DeleteButton).vm.$emit('deleteSelectedDesigns');
- expect(wrapper.emitted().delete).toBeTruthy();
- });
- });
-
- it('renders download button with correct link', () => {
- expect(wrapper.find(GlDeprecatedButton).attributes('href')).toBe(
- '/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
- );
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js b/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js
deleted file mode 100644
index d2153adca45..00000000000
--- a/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import VueRouter from 'vue-router';
-import PaginationButton from '~/design_management_legacy/components/toolbar/pagination_button.vue';
-import { DESIGN_ROUTE_NAME } from '~/design_management_legacy/router/constants';
-
-const localVue = createLocalVue();
-localVue.use(VueRouter);
-const router = new VueRouter();
-
-describe('Design management pagination button component', () => {
- let wrapper;
-
- function createComponent(design = null) {
- wrapper = shallowMount(PaginationButton, {
- localVue,
- router,
- propsData: {
- design,
- title: 'Test title',
- iconName: 'angle-right',
- },
- stubs: ['router-link'],
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('disables button when no design is passed', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders router-link', () => {
- createComponent({ id: '2' });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('designLink', () => {
- it('returns empty link when design is null', () => {
- createComponent();
-
- expect(wrapper.vm.designLink).toEqual({});
- });
-
- it('returns design link', () => {
- createComponent({ id: '2', filename: 'test' });
-
- wrapper.vm.$router.replace('/root/test-project/issues/1/designs/test?version=1');
-
- expect(wrapper.vm.designLink).toEqual({
- name: DESIGN_ROUTE_NAME,
- params: { id: 'test' },
- query: { version: '1' },
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js b/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js
deleted file mode 100644
index 21b55113a6e..00000000000
--- a/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/* global Mousetrap */
-import 'mousetrap';
-import { shallowMount } from '@vue/test-utils';
-import Pagination from '~/design_management_legacy/components/toolbar/pagination.vue';
-import { DESIGN_ROUTE_NAME } from '~/design_management_legacy/router/constants';
-
-const push = jest.fn();
-const $router = {
- push,
-};
-
-const $route = {
- path: '/designs/design-2',
- query: {},
-};
-
-describe('Design management pagination component', () => {
- let wrapper;
-
- function createComponent() {
- wrapper = shallowMount(Pagination, {
- propsData: {
- id: '2',
- },
- mocks: {
- $router,
- $route,
- },
- });
- }
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('hides components when designs are empty', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders pagination buttons', () => {
- wrapper.setData({
- designs: [{ id: '1' }, { id: '2' }],
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- describe('keyboard buttons navigation', () => {
- beforeEach(() => {
- wrapper.setData({
- designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }],
- });
- });
-
- it('routes to previous design on Left button', () => {
- Mousetrap.trigger('left');
- expect(push).toHaveBeenCalledWith({
- name: DESIGN_ROUTE_NAME,
- params: { id: '1' },
- query: {},
- });
- });
-
- it('routes to next design on Right button', () => {
- Mousetrap.trigger('right');
- expect(push).toHaveBeenCalledWith({
- name: DESIGN_ROUTE_NAME,
- params: { id: '3' },
- query: {},
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap
deleted file mode 100644
index 27c0ba589e6..00000000000
--- a/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap
+++ /dev/null
@@ -1,79 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management upload button component renders inverted upload design button 1`] = `
-<div
- isinverted="true"
->
- <gl-deprecated-button-stub
- size="md"
- title="Adding a design with the same filename replaces the file in a new version."
- variant="success"
- >
-
- Upload designs
-
- <!---->
- </gl-deprecated-button-stub>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-</div>
-`;
-
-exports[`Design management upload button component renders loading icon 1`] = `
-<div>
- <gl-deprecated-button-stub
- disabled="true"
- size="md"
- title="Adding a design with the same filename replaces the file in a new version."
- variant="success"
- >
-
- Upload designs
-
- <gl-loading-icon-stub
- class="ml-1"
- color="orange"
- inline="true"
- label="Loading"
- size="sm"
- />
- </gl-deprecated-button-stub>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-</div>
-`;
-
-exports[`Design management upload button component renders upload design button 1`] = `
-<div>
- <gl-deprecated-button-stub
- size="md"
- title="Adding a design with the same filename replaces the file in a new version."
- variant="success"
- >
-
- Upload designs
-
- <!---->
- </gl-deprecated-button-stub>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap
deleted file mode 100644
index 0737b9729a2..00000000000
--- a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap
+++ /dev/null
@@ -1,455 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management dropzone component when dragging renders correct template when drag event contains files 1`] = `
-<div
- class="w-100 position-relative"
->
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- >
- <div
- class="d-flex-center flex-column text-center"
- >
- <gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
- />
-
- <p>
- <gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
- />
- </p>
- </div>
- </button>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-
- <transition-stub
- name="design-dropzone-fade"
- >
- <div
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- style=""
- >
- <div
- class="mw-50 text-center"
- style="display: none;"
- >
- <h3>
- Oh no!
- </h3>
-
- <span>
- You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
- </span>
- </div>
-
- <div
- class="mw-50 text-center"
- style=""
- >
- <h3>
- Incoming!
- </h3>
-
- <span>
- Drop your designs to start your upload.
- </span>
- </div>
- </div>
- </transition-stub>
-</div>
-`;
-
-exports[`Design management dropzone component when dragging renders correct template when drag event contains files and text 1`] = `
-<div
- class="w-100 position-relative"
->
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- >
- <div
- class="d-flex-center flex-column text-center"
- >
- <gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
- />
-
- <p>
- <gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
- />
- </p>
- </div>
- </button>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-
- <transition-stub
- name="design-dropzone-fade"
- >
- <div
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- style=""
- >
- <div
- class="mw-50 text-center"
- style="display: none;"
- >
- <h3>
- Oh no!
- </h3>
-
- <span>
- You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
- </span>
- </div>
-
- <div
- class="mw-50 text-center"
- style=""
- >
- <h3>
- Incoming!
- </h3>
-
- <span>
- Drop your designs to start your upload.
- </span>
- </div>
- </div>
- </transition-stub>
-</div>
-`;
-
-exports[`Design management dropzone component when dragging renders correct template when drag event contains text 1`] = `
-<div
- class="w-100 position-relative"
->
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- >
- <div
- class="d-flex-center flex-column text-center"
- >
- <gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
- />
-
- <p>
- <gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
- />
- </p>
- </div>
- </button>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-
- <transition-stub
- name="design-dropzone-fade"
- >
- <div
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- style=""
- >
- <div
- class="mw-50 text-center"
- >
- <h3>
- Oh no!
- </h3>
-
- <span>
- You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
- </span>
- </div>
-
- <div
- class="mw-50 text-center"
- style="display: none;"
- >
- <h3>
- Incoming!
- </h3>
-
- <span>
- Drop your designs to start your upload.
- </span>
- </div>
- </div>
- </transition-stub>
-</div>
-`;
-
-exports[`Design management dropzone component when dragging renders correct template when drag event is empty 1`] = `
-<div
- class="w-100 position-relative"
->
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- >
- <div
- class="d-flex-center flex-column text-center"
- >
- <gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
- />
-
- <p>
- <gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
- />
- </p>
- </div>
- </button>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-
- <transition-stub
- name="design-dropzone-fade"
- >
- <div
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- style=""
- >
- <div
- class="mw-50 text-center"
- >
- <h3>
- Oh no!
- </h3>
-
- <span>
- You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
- </span>
- </div>
-
- <div
- class="mw-50 text-center"
- style="display: none;"
- >
- <h3>
- Incoming!
- </h3>
-
- <span>
- Drop your designs to start your upload.
- </span>
- </div>
- </div>
- </transition-stub>
-</div>
-`;
-
-exports[`Design management dropzone component when dragging renders correct template when dragging stops 1`] = `
-<div
- class="w-100 position-relative"
->
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- >
- <div
- class="d-flex-center flex-column text-center"
- >
- <gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
- />
-
- <p>
- <gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
- />
- </p>
- </div>
- </button>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-
- <transition-stub
- name="design-dropzone-fade"
- >
- <div
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- style="display: none;"
- >
- <div
- class="mw-50 text-center"
- >
- <h3>
- Oh no!
- </h3>
-
- <span>
- You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
- </span>
- </div>
-
- <div
- class="mw-50 text-center"
- style="display: none;"
- >
- <h3>
- Incoming!
- </h3>
-
- <span>
- Drop your designs to start your upload.
- </span>
- </div>
- </div>
- </transition-stub>
-</div>
-`;
-
-exports[`Design management dropzone component when no slot provided renders default dropzone card 1`] = `
-<div
- class="w-100 position-relative"
->
- <button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
- >
- <div
- class="d-flex-center flex-column text-center"
- >
- <gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
- />
-
- <p>
- <gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
- />
- </p>
- </div>
- </button>
-
- <input
- accept="image/*"
- class="hide"
- multiple="multiple"
- name="design_file"
- type="file"
- />
-
- <transition-stub
- name="design-dropzone-fade"
- >
- <div
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- style="display: none;"
- >
- <div
- class="mw-50 text-center"
- >
- <h3>
- Oh no!
- </h3>
-
- <span>
- You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
- </span>
- </div>
-
- <div
- class="mw-50 text-center"
- style="display: none;"
- >
- <h3>
- Incoming!
- </h3>
-
- <span>
- Drop your designs to start your upload.
- </span>
- </div>
- </div>
- </transition-stub>
-</div>
-`;
-
-exports[`Design management dropzone component when slot provided renders dropzone with slot content 1`] = `
-<div
- class="w-100 position-relative"
->
- <div>
- dropzone slot
- </div>
-
- <transition-stub
- name="design-dropzone-fade"
- >
- <div
- class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
- style="display: none;"
- >
- <div
- class="mw-50 text-center"
- >
- <h3>
- Oh no!
- </h3>
-
- <span>
- You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
- </span>
- </div>
-
- <div
- class="mw-50 text-center"
- style="display: none;"
- >
- <h3>
- Incoming!
- </h3>
-
- <span>
- Drop your designs to start your upload.
- </span>
- </div>
- </div>
- </transition-stub>
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
deleted file mode 100644
index d34b925f33d..00000000000
--- a/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ /dev/null
@@ -1,111 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-deprecated-dropdown-stub
- class="design-version-dropdown"
- issueiid=""
- projectpath=""
- text="Showing Latest Version"
- variant="link"
->
- <gl-deprecated-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 2
-
- <span>
- (latest)
- </span>
- </strong>
- </div>
- </div>
-
- <i
- class="fa fa-check float-right gl-mr-2"
- />
- </router-link-stub>
- </gl-deprecated-dropdown-item-stub>
- <gl-deprecated-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 1
-
- <!---->
- </strong>
- </div>
- </div>
-
- <!---->
- </router-link-stub>
- </gl-deprecated-dropdown-item-stub>
-</gl-deprecated-dropdown-stub>
-`;
-
-exports[`Design management design version dropdown component renders design version list 1`] = `
-<gl-deprecated-dropdown-stub
- class="design-version-dropdown"
- issueiid=""
- projectpath=""
- text="Showing Latest Version"
- variant="link"
->
- <gl-deprecated-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 2
-
- <span>
- (latest)
- </span>
- </strong>
- </div>
- </div>
-
- <i
- class="fa fa-check float-right gl-mr-2"
- />
- </router-link-stub>
- </gl-deprecated-dropdown-item-stub>
- <gl-deprecated-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 1
-
- <!---->
- </strong>
- </div>
- </div>
-
- <!---->
- </router-link-stub>
- </gl-deprecated-dropdown-item-stub>
-</gl-deprecated-dropdown-stub>
-`;
diff --git a/spec/frontend/design_management_legacy/components/upload/button_spec.js b/spec/frontend/design_management_legacy/components/upload/button_spec.js
deleted file mode 100644
index dde5c694194..00000000000
--- a/spec/frontend/design_management_legacy/components/upload/button_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import UploadButton from '~/design_management_legacy/components/upload/button.vue';
-
-describe('Design management upload button component', () => {
- let wrapper;
-
- function createComponent(isSaving = false, isInverted = false) {
- wrapper = shallowMount(UploadButton, {
- propsData: {
- isSaving,
- isInverted,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders upload design button', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders inverted upload design button', () => {
- createComponent(false, true);
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders loading icon', () => {
- createComponent(true);
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('onFileUploadChange', () => {
- it('emits upload event', () => {
- createComponent();
-
- wrapper.vm.onFileUploadChange({ target: { files: 'test' } });
-
- expect(wrapper.emitted().upload[0]).toEqual(['test']);
- });
- });
-
- describe('openFileUpload', () => {
- it('triggers click on input', () => {
- createComponent();
-
- const clickSpy = jest.spyOn(wrapper.find('input').element, 'click');
-
- wrapper.vm.openFileUpload();
-
- expect(clickSpy).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js b/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js
deleted file mode 100644
index 1907a3124a6..00000000000
--- a/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import DesignDropzone from '~/design_management_legacy/components/upload/design_dropzone.vue';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-
-jest.mock('~/flash');
-
-describe('Design management dropzone component', () => {
- let wrapper;
-
- const mockDragEvent = ({ types = ['Files'], files = [] }) => {
- return { dataTransfer: { types, files } };
- };
-
- const findDropzoneCard = () => wrapper.find('.design-dropzone-card');
-
- function createComponent({ slots = {}, data = {} } = {}) {
- wrapper = shallowMount(DesignDropzone, {
- slots,
- data() {
- return data;
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when slot provided', () => {
- it('renders dropzone with slot content', () => {
- createComponent({
- slots: {
- default: ['<div>dropzone slot</div>'],
- },
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- describe('when no slot provided', () => {
- it('renders default dropzone card', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('triggers click event on file input element when clicked', () => {
- createComponent();
- const clickSpy = jest.spyOn(wrapper.find('input').element, 'click');
-
- findDropzoneCard().trigger('click');
- expect(clickSpy).toHaveBeenCalled();
- });
- });
-
- describe('when dragging', () => {
- it.each`
- description | eventPayload
- ${'is empty'} | ${{}}
- ${'contains text'} | ${mockDragEvent({ types: ['text'] })}
- ${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })}
- ${'contains files'} | ${mockDragEvent({ types: ['Files'] })}
- `('renders correct template when drag event $description', ({ eventPayload }) => {
- createComponent();
-
- wrapper.trigger('dragenter', eventPayload);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders correct template when dragging stops', () => {
- createComponent();
-
- wrapper.trigger('dragenter');
- return wrapper.vm
- .$nextTick()
- .then(() => {
- wrapper.trigger('dragleave');
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
- });
-
- describe('when dropping', () => {
- it('emits upload event', () => {
- createComponent();
- const mockFile = { name: 'test', type: 'image/jpg' };
- const mockEvent = mockDragEvent({ files: [mockFile] });
-
- wrapper.trigger('dragenter', mockEvent);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- wrapper.trigger('drop', mockEvent);
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
- });
- });
- });
-
- describe('ondrop', () => {
- const mockData = { dragCounter: 1, isDragDataValid: true };
-
- describe('when drag data is valid', () => {
- it('emits upload event for valid files', () => {
- createComponent({ data: mockData });
-
- const mockFile = { type: 'image/jpg' };
- const mockEvent = mockDragEvent({ files: [mockFile] });
-
- wrapper.vm.ondrop(mockEvent);
- expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
- });
-
- it('calls createFlash when files are invalid', () => {
- createComponent({ data: mockData });
-
- const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
-
- wrapper.vm.ondrop(mockEvent);
- expect(createFlash).toHaveBeenCalledTimes(1);
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js
deleted file mode 100644
index 7fb85f357c7..00000000000
--- a/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
-import DesignVersionDropdown from '~/design_management_legacy/components/upload/design_version_dropdown.vue';
-import mockAllVersions from './mock_data/all_versions';
-
-const LATEST_VERSION_ID = 3;
-const PREVIOUS_VERSION_ID = 2;
-
-const designRouteFactory = versionId => ({
- path: `/designs?version=${versionId}`,
- query: {
- version: `${versionId}`,
- },
-});
-
-const MOCK_ROUTE = {
- path: '/designs',
- query: {},
-};
-
-describe('Design management design version dropdown component', () => {
- let wrapper;
-
- function createComponent({ maxVersions = -1, $route = MOCK_ROUTE } = {}) {
- wrapper = shallowMount(DesignVersionDropdown, {
- propsData: {
- projectPath: '',
- issueIid: '',
- },
- mocks: {
- $route,
- },
- stubs: ['router-link'],
- });
-
- wrapper.setData({
- allVersions: maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions,
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findVersionLink = index => wrapper.findAll('.js-version-link').at(index);
-
- it('renders design version dropdown button', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders design version list', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- describe('selected version name', () => {
- it('has "latest" on most recent version item', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findVersionLink(0).text()).toContain('latest');
- });
- });
- });
-
- describe('versions list', () => {
- it('displays latest version text by default', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(
- 'Showing Latest Version',
- );
- });
- });
-
- it('displays latest version text when only 1 version is present', () => {
- createComponent({ maxVersions: 1 });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(
- 'Showing Latest Version',
- );
- });
- });
-
- it('displays version text when the current version is not the latest', () => {
- createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(`Showing Version #1`);
- });
- });
-
- it('displays latest version text when the current version is the latest', () => {
- createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(
- 'Showing Latest Version',
- );
- });
- });
-
- it('should have the same length as apollo query', () => {
- createComponent();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlDeprecatedDropdownItem)).toHaveLength(
- wrapper.vm.allVersions.length,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js b/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js
deleted file mode 100644
index e76bbd261bd..00000000000
--- a/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default [
- {
- node: {
- id: 'gid://gitlab/DesignManagement::Version/3',
- sha: '0945756378e0b1588b9dd40d5a6b99e8b7198f55',
- },
- },
- {
- node: {
- id: 'gid://gitlab/DesignManagement::Version/2',
- sha: '5b063fef0cd7213b312db65b30e24f057df21b20',
- },
- },
-];
diff --git a/spec/frontend/design_management_legacy/mock_data/all_versions.js b/spec/frontend/design_management_legacy/mock_data/all_versions.js
deleted file mode 100644
index c389fdb8747..00000000000
--- a/spec/frontend/design_management_legacy/mock_data/all_versions.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default [
- {
- node: {
- id: 'gid://gitlab/DesignManagement::Version/1',
- sha: 'b389071a06c153509e11da1f582005b316667001',
- },
- },
-];
diff --git a/spec/frontend/design_management_legacy/mock_data/design.js b/spec/frontend/design_management_legacy/mock_data/design.js
deleted file mode 100644
index 675198b9408..00000000000
--- a/spec/frontend/design_management_legacy/mock_data/design.js
+++ /dev/null
@@ -1,74 +0,0 @@
-export default {
- id: 'design-id',
- filename: 'test.jpg',
- fullPath: 'full-design-path',
- image: 'test.jpg',
- updatedAt: '01-01-2019',
- updatedBy: {
- name: 'test',
- },
- issue: {
- title: 'My precious issue',
- webPath: 'full-issue-path',
- webUrl: 'full-issue-url',
- participants: {
- edges: [
- {
- node: {
- name: 'Administrator',
- username: 'root',
- webUrl: 'link-to-author',
- avatarUrl: 'link-to-avatar',
- },
- },
- ],
- },
- },
- discussions: {
- nodes: [
- {
- id: 'discussion-id',
- replyId: 'discussion-reply-id',
- resolved: false,
- notes: {
- nodes: [
- {
- id: 'note-id',
- body: '123',
- author: {
- name: 'Administrator',
- username: 'root',
- webUrl: 'link-to-author',
- avatarUrl: 'link-to-avatar',
- },
- },
- ],
- },
- },
- {
- id: 'discussion-resolved',
- replyId: 'discussion-reply-resolved',
- resolved: true,
- notes: {
- nodes: [
- {
- id: 'note-resolved',
- body: '123',
- author: {
- name: 'Administrator',
- username: 'root',
- webUrl: 'link-to-author',
- avatarUrl: 'link-to-avatar',
- },
- },
- ],
- },
- },
- ],
- },
- diffRefs: {
- headSha: 'headSha',
- baseSha: 'baseSha',
- startSha: 'startSha',
- },
-};
diff --git a/spec/frontend/design_management_legacy/mock_data/designs.js b/spec/frontend/design_management_legacy/mock_data/designs.js
deleted file mode 100644
index 07f5c1b7457..00000000000
--- a/spec/frontend/design_management_legacy/mock_data/designs.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import design from './design';
-
-export default {
- project: {
- issue: {
- designCollection: {
- designs: {
- edges: [
- {
- node: design,
- },
- ],
- },
- },
- },
- },
-};
diff --git a/spec/frontend/design_management_legacy/mock_data/no_designs.js b/spec/frontend/design_management_legacy/mock_data/no_designs.js
deleted file mode 100644
index 9db0ffcade2..00000000000
--- a/spec/frontend/design_management_legacy/mock_data/no_designs.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export default {
- project: {
- issue: {
- designCollection: {
- designs: {
- edges: [],
- },
- },
- },
- },
-};
diff --git a/spec/frontend/design_management_legacy/mock_data/notes.js b/spec/frontend/design_management_legacy/mock_data/notes.js
deleted file mode 100644
index 80cb3944786..00000000000
--- a/spec/frontend/design_management_legacy/mock_data/notes.js
+++ /dev/null
@@ -1,46 +0,0 @@
-export default [
- {
- id: 'note-id-1',
- index: 1,
- position: {
- height: 100,
- width: 100,
- x: 10,
- y: 15,
- },
- author: {
- name: 'John',
- webUrl: 'link-to-john-profile',
- },
- createdAt: '2020-05-08T07:10:45Z',
- userPermissions: {
- adminNote: true,
- },
- discussion: {
- id: 'discussion-id-1',
- },
- resolved: false,
- },
- {
- id: 'note-id-2',
- index: 2,
- position: {
- height: 50,
- width: 50,
- x: 25,
- y: 25,
- },
- author: {
- name: 'Mary',
- webUrl: 'link-to-mary-profile',
- },
- createdAt: '2020-05-08T07:10:45Z',
- userPermissions: {
- adminNote: true,
- },
- discussion: {
- id: 'discussion-id-2',
- },
- resolved: true,
- },
-];
diff --git a/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap
deleted file mode 100644
index 3ba63fd14f0..00000000000
--- a/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap
+++ /dev/null
@@ -1,263 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
-<div>
- <!---->
-
- <div
- class="mt-4"
- >
- <ol
- class="list-unstyled row"
- >
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub
- class="design-list-item"
- />
- </li>
-
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub>
- <design-stub
- event="NONE"
- filename="design-1-name"
- id="design-1"
- image="design-1-image"
- notescount="0"
- />
- </design-dropzone-stub>
-
- <!---->
- </li>
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub>
- <design-stub
- event="NONE"
- filename="design-2-name"
- id="design-2"
- image="design-2-image"
- notescount="1"
- />
- </design-dropzone-stub>
-
- <!---->
- </li>
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub>
- <design-stub
- event="NONE"
- filename="design-3-name"
- id="design-3"
- image="design-3-image"
- notescount="0"
- />
- </design-dropzone-stub>
-
- <!---->
- </li>
- </ol>
- </div>
-
- <router-view-stub
- name="default"
- />
-</div>
-`;
-
-exports[`Design management index page designs renders designs list and header with upload button 1`] = `
-<div>
- <header
- class="row-content-block border-top-0 p-2 d-flex"
- >
- <div
- class="d-flex justify-content-between align-items-center w-100"
- >
- <design-version-dropdown-stub />
-
- <div
- class="qa-selector-toolbar d-flex"
- >
- <gl-deprecated-button-stub
- class="mr-2 js-select-all"
- size="md"
- variant="link"
- >
- Select all
- </gl-deprecated-button-stub>
-
- <div>
- <delete-button-stub
- buttonclass="btn-danger btn-inverted mr-2"
- buttonvariant=""
- >
-
- Delete selected
-
- <!---->
- </delete-button-stub>
- </div>
-
- <upload-button-stub />
- </div>
- </div>
- </header>
-
- <div
- class="mt-4"
- >
- <ol
- class="list-unstyled row"
- >
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub
- class="design-list-item"
- />
- </li>
-
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub>
- <design-stub
- event="NONE"
- filename="design-1-name"
- id="design-1"
- image="design-1-image"
- notescount="0"
- />
- </design-dropzone-stub>
-
- <input
- class="design-checkbox"
- type="checkbox"
- />
- </li>
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub>
- <design-stub
- event="NONE"
- filename="design-2-name"
- id="design-2"
- image="design-2-image"
- notescount="1"
- />
- </design-dropzone-stub>
-
- <input
- class="design-checkbox"
- type="checkbox"
- />
- </li>
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub>
- <design-stub
- event="NONE"
- filename="design-3-name"
- id="design-3"
- image="design-3-image"
- notescount="0"
- />
- </design-dropzone-stub>
-
- <input
- class="design-checkbox"
- type="checkbox"
- />
- </li>
- </ol>
- </div>
-
- <router-view-stub
- name="default"
- />
-</div>
-`;
-
-exports[`Design management index page designs renders error 1`] = `
-<div>
- <!---->
-
- <div
- class="mt-4"
- >
- <gl-alert-stub
- dismisslabel="Dismiss"
- primarybuttonlink=""
- primarybuttontext=""
- secondarybuttonlink=""
- secondarybuttontext=""
- title=""
- variant="danger"
- >
-
- An error occurred while loading designs. Please try again.
-
- </gl-alert-stub>
- </div>
-
- <router-view-stub
- name="default"
- />
-</div>
-`;
-
-exports[`Design management index page designs renders loading icon 1`] = `
-<div>
- <!---->
-
- <div
- class="mt-4"
- >
- <gl-loading-icon-stub
- color="orange"
- label="Loading"
- size="md"
- />
- </div>
-
- <router-view-stub
- name="default"
- />
-</div>
-`;
-
-exports[`Design management index page when has no designs renders empty text 1`] = `
-<div>
- <!---->
-
- <div
- class="mt-4"
- >
- <ol
- class="list-unstyled row"
- >
- <li
- class="col-md-6 col-lg-4 mb-3"
- >
- <design-dropzone-stub
- class="design-list-item"
- />
- </li>
-
- </ol>
- </div>
-
- <router-view-stub
- name="default"
- />
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap
deleted file mode 100644
index dc5baf37fc6..00000000000
--- a/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap
+++ /dev/null
@@ -1,216 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management design index page renders design index 1`] = `
-<div
- class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
->
- <div
- class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"
- >
- <design-destroyer-stub
- filenames="test.jpg"
- iid="1"
- projectpath=""
- />
-
- <!---->
-
- <design-presentation-stub
- discussions="[object Object],[object Object]"
- image="test.jpg"
- imagename="test.jpg"
- scale="1"
- />
-
- <div
- class="design-scaler-wrapper position-absolute mb-4 d-flex-center"
- >
- <design-scaler-stub />
- </div>
- </div>
-
- <div
- class="image-notes"
- >
- <h2
- class="gl-font-weight-bold gl-mt-0"
- >
-
- My precious issue
-
- </h2>
-
- <a
- class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
- href="full-issue-url"
- >
- ull-issue-path
- </a>
-
- <participants-stub
- class="gl-mb-4"
- numberoflessparticipants="7"
- participants="[object Object]"
- />
-
- <!---->
-
- <design-discussion-stub
- data-testid="unresolved-discussion"
- designid="test"
- discussion="[object Object]"
- discussionwithopenform=""
- markdownpreviewpath="//preview_markdown?target_type=Issue"
- noteableid="design-id"
- />
-
- <gl-button-stub
- category="primary"
- class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
- data-testid="resolved-comments"
- icon="chevron-right"
- id="resolved-comments"
- size="medium"
- variant="link"
- >
- Resolved Comments (1)
-
- </gl-button-stub>
-
- <gl-popover-stub
- container="popovercontainer"
- cssclasses=""
- placement="top"
- show="true"
- target="resolved-comments"
- title="Resolved Comments"
- >
- <p>
-
- Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below
-
- </p>
-
- <a
- href="#"
- rel="noopener noreferrer"
- target="_blank"
- >
- Learn more about resolving comments
- </a>
- </gl-popover-stub>
-
- <gl-collapse-stub
- class="gl-mt-3"
- >
- <design-discussion-stub
- data-testid="resolved-discussion"
- designid="test"
- discussion="[object Object]"
- discussionwithopenform=""
- markdownpreviewpath="//preview_markdown?target_type=Issue"
- noteableid="design-id"
- />
- </gl-collapse-stub>
-
- </div>
-</div>
-`;
-
-exports[`Design management design index page sets loading state 1`] = `
-<div
- class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
->
- <gl-loading-icon-stub
- class="align-self-center"
- color="orange"
- label="Loading"
- size="xl"
- />
-</div>
-`;
-
-exports[`Design management design index page with error GlAlert is rendered in correct position with correct content 1`] = `
-<div
- class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
->
- <div
- class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"
- >
- <design-destroyer-stub
- filenames="test.jpg"
- iid="1"
- projectpath=""
- />
-
- <div
- class="p-3"
- >
- <gl-alert-stub
- dismissible="true"
- dismisslabel="Dismiss"
- primarybuttonlink=""
- primarybuttontext=""
- secondarybuttonlink=""
- secondarybuttontext=""
- title=""
- variant="danger"
- >
-
- woops
-
- </gl-alert-stub>
- </div>
-
- <design-presentation-stub
- discussions=""
- image="test.jpg"
- imagename="test.jpg"
- scale="1"
- />
-
- <div
- class="design-scaler-wrapper position-absolute mb-4 d-flex-center"
- >
- <design-scaler-stub />
- </div>
- </div>
-
- <div
- class="image-notes"
- >
- <h2
- class="gl-font-weight-bold gl-mt-0"
- >
-
- My precious issue
-
- </h2>
-
- <a
- class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
- href="full-issue-url"
- >
- ull-issue-path
- </a>
-
- <participants-stub
- class="gl-mb-4"
- numberoflessparticipants="7"
- participants="[object Object]"
- />
-
- <h2
- class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
- data-testid="new-discussion-disclaimer"
- >
-
- Click the image where you'd like to start a new discussion
-
- </h2>
-
- <!---->
-
- </div>
-</div>
-`;
diff --git a/spec/frontend/design_management_legacy/pages/design/index_spec.js b/spec/frontend/design_management_legacy/pages/design/index_spec.js
deleted file mode 100644
index 5eb4158c715..00000000000
--- a/spec/frontend/design_management_legacy/pages/design/index_spec.js
+++ /dev/null
@@ -1,291 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import VueRouter from 'vue-router';
-import { GlAlert } from '@gitlab/ui';
-import { ApolloMutation } from 'vue-apollo';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import DesignIndex from '~/design_management_legacy/pages/design/index.vue';
-import DesignSidebar from '~/design_management_legacy/components/design_sidebar.vue';
-import DesignPresentation from '~/design_management_legacy/components/design_presentation.vue';
-import createImageDiffNoteMutation from '~/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql';
-import design from '../../mock_data/design';
-import mockResponseWithDesigns from '../../mock_data/designs';
-import mockResponseNoDesigns from '../../mock_data/no_designs';
-import mockAllVersions from '../../mock_data/all_versions';
-import {
- DESIGN_NOT_FOUND_ERROR,
- DESIGN_VERSION_NOT_EXIST_ERROR,
-} from '~/design_management_legacy/utils/error_messages';
-import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants';
-import createRouter from '~/design_management_legacy/router';
-import * as utils from '~/design_management_legacy/utils/design_management_utils';
-import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_legacy/constants';
-
-jest.mock('~/flash');
-jest.mock('mousetrap', () => ({
- bind: jest.fn(),
- unbind: jest.fn(),
-}));
-
-const focusInput = jest.fn();
-
-const DesignReplyForm = {
- template: '<div><textarea ref="textarea"></textarea></div>',
- methods: {
- focusInput,
- },
-};
-
-const localVue = createLocalVue();
-localVue.use(VueRouter);
-
-describe('Design management design index page', () => {
- let wrapper;
- let router;
-
- const newComment = 'new comment';
- const annotationCoordinates = {
- x: 10,
- y: 10,
- width: 100,
- height: 100,
- };
- const createDiscussionMutationVariables = {
- mutation: createImageDiffNoteMutation,
- update: expect.anything(),
- variables: {
- input: {
- body: newComment,
- noteableId: design.id,
- position: {
- headSha: 'headSha',
- baseSha: 'baseSha',
- startSha: 'startSha',
- paths: {
- newPath: 'full-design-path',
- },
- ...annotationCoordinates,
- },
- },
- },
- };
-
- const mutate = jest.fn().mockResolvedValue();
-
- const findDiscussionForm = () => wrapper.find(DesignReplyForm);
- const findSidebar = () => wrapper.find(DesignSidebar);
- const findDesignPresentation = () => wrapper.find(DesignPresentation);
-
- function createComponent(loading = false, data = {}) {
- const $apollo = {
- queries: {
- design: {
- loading,
- },
- },
- mutate,
- };
-
- router = createRouter();
-
- wrapper = shallowMount(DesignIndex, {
- propsData: { id: '1' },
- mocks: { $apollo },
- stubs: {
- ApolloMutation,
- DesignSidebar,
- DesignReplyForm,
- },
- data() {
- return {
- issueIid: '1',
- activeDiscussion: {
- id: null,
- source: null,
- },
- ...data,
- };
- },
- localVue,
- router,
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when navigating', () => {
- it('applies fullscreen layout', () => {
- const mockEl = {
- classList: {
- add: jest.fn(),
- remove: jest.fn(),
- },
- };
- jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl);
- createComponent(true);
-
- wrapper.vm.$router.push('/designs/test');
- expect(mockEl.classList.add).toHaveBeenCalledTimes(1);
- expect(mockEl.classList.add).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
- });
- });
-
- it('sets loading state', () => {
- createComponent(true);
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders design index', () => {
- createComponent(false, { design });
-
- expect(wrapper.element).toMatchSnapshot();
- expect(wrapper.find(GlAlert).exists()).toBe(false);
- });
-
- it('passes correct props to sidebar component', () => {
- createComponent(false, { design });
-
- expect(findSidebar().props()).toEqual({
- design,
- markdownPreviewPath: '//preview_markdown?target_type=Issue',
- resolvedDiscussionsExpanded: false,
- });
- });
-
- it('opens a new discussion form', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
- },
- },
- });
-
- findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findDiscussionForm().exists()).toBe(true);
- });
- });
-
- it('keeps new discussion form focused', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
- },
- },
- annotationCoordinates,
- });
-
- findDesignPresentation().vm.$emit('openCommentForm', { x: 10, y: 10 });
-
- expect(focusInput).toHaveBeenCalled();
- });
-
- it('sends a mutation on submitting form and closes form', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
- },
- },
- annotationCoordinates,
- comment: newComment,
- });
-
- findDiscussionForm().vm.$emit('submitForm');
- expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables);
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- return mutate({ variables: createDiscussionMutationVariables });
- })
- .then(() => {
- expect(findDiscussionForm().exists()).toBe(false);
- });
- });
-
- it('closes the form and clears the comment on canceling form', () => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
- },
- },
- annotationCoordinates,
- comment: newComment,
- });
-
- findDiscussionForm().vm.$emit('cancelForm');
-
- expect(wrapper.vm.comment).toBe('');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findDiscussionForm().exists()).toBe(false);
- });
- });
-
- describe('with error', () => {
- beforeEach(() => {
- createComponent(false, {
- design: {
- ...design,
- discussions: {
- nodes: [],
- },
- },
- errorMessage: 'woops',
- });
- });
-
- it('GlAlert is rendered in correct position with correct content', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- describe('onDesignQueryResult', () => {
- describe('with no designs', () => {
- it('redirects to /designs', () => {
- createComponent(true);
- router.push = jest.fn();
-
- wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(DESIGN_NOT_FOUND_ERROR);
- expect(router.push).toHaveBeenCalledTimes(1);
- expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
- });
- });
- });
-
- describe('when no design exists for given version', () => {
- it('redirects to /designs', () => {
- createComponent(true);
- wrapper.setData({
- allVersions: mockAllVersions,
- });
-
- // attempt to query for a version of the design that doesn't exist
- router.push({ query: { version: '999' } });
- router.push = jest.fn();
-
- wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(DESIGN_VERSION_NOT_EXIST_ERROR);
- expect(router.push).toHaveBeenCalledTimes(1);
- expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
- });
- });
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/pages/index_spec.js b/spec/frontend/design_management_legacy/pages/index_spec.js
deleted file mode 100644
index 5b7512aab7b..00000000000
--- a/spec/frontend/design_management_legacy/pages/index_spec.js
+++ /dev/null
@@ -1,543 +0,0 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
-import VueRouter from 'vue-router';
-import { GlEmptyState } from '@gitlab/ui';
-import Index from '~/design_management_legacy/pages/index.vue';
-import uploadDesignQuery from '~/design_management_legacy/graphql/mutations/upload_design.mutation.graphql';
-import DesignDestroyer from '~/design_management_legacy/components/design_destroyer.vue';
-import DesignDropzone from '~/design_management_legacy/components/upload/design_dropzone.vue';
-import DeleteButton from '~/design_management_legacy/components/delete_button.vue';
-import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants';
-import {
- EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
- EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
-} from '~/design_management_legacy/utils/error_messages';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import createRouter from '~/design_management_legacy/router';
-import * as utils from '~/design_management_legacy/utils/design_management_utils';
-import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_legacy/constants';
-
-jest.mock('~/flash.js');
-const mockPageEl = {
- classList: {
- remove: jest.fn(),
- },
-};
-jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageEl);
-
-const localVue = createLocalVue();
-const router = createRouter();
-localVue.use(VueRouter);
-
-const mockDesigns = [
- {
- id: 'design-1',
- image: 'design-1-image',
- filename: 'design-1-name',
- event: 'NONE',
- notesCount: 0,
- },
- {
- id: 'design-2',
- image: 'design-2-image',
- filename: 'design-2-name',
- event: 'NONE',
- notesCount: 1,
- },
- {
- id: 'design-3',
- image: 'design-3-image',
- filename: 'design-3-name',
- event: 'NONE',
- notesCount: 0,
- },
-];
-
-const mockVersion = {
- node: {
- id: 'gid://gitlab/DesignManagement::Version/1',
- },
-};
-
-describe('Design management index page', () => {
- let mutate;
- let wrapper;
-
- const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
- const findSelectAllButton = () => wrapper.find('.js-select-all');
- const findToolbar = () => wrapper.find('.qa-selector-toolbar');
- const findDeleteButton = () => wrapper.find(DeleteButton);
- const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
- const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
-
- function createComponent({
- loading = false,
- designs = [],
- allVersions = [],
- createDesign = true,
- stubs = {},
- mockMutate = jest.fn().mockResolvedValue(),
- } = {}) {
- mutate = mockMutate;
- const $apollo = {
- queries: {
- designs: {
- loading,
- },
- permissions: {
- loading,
- },
- },
- mutate,
- };
-
- wrapper = shallowMount(Index, {
- mocks: { $apollo },
- localVue,
- router,
- stubs: { DesignDestroyer, ApolloMutation, ...stubs },
- attachToDocument: true,
- });
-
- wrapper.setData({
- designs,
- allVersions,
- issueIid: '1',
- permissions: {
- createDesign,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('designs', () => {
- it('renders loading icon', () => {
- createComponent({ loading: true });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders error', () => {
- createComponent();
-
- wrapper.setData({ error: true });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders a toolbar with buttons when there are designs', () => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findToolbar().exists()).toBe(true);
- });
- });
-
- it('renders designs list and header with upload button', () => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('does not render toolbar when there is no permission', () => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
- });
-
- describe('when has no designs', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders empty text', () =>
- wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- }));
-
- it('does not render a toolbar with buttons', () =>
- wrapper.vm.$nextTick().then(() => {
- expect(findToolbar().exists()).toBe(false);
- }));
- });
-
- describe('uploading designs', () => {
- it('calls mutation on upload', () => {
- createComponent({ stubs: { GlEmptyState } });
-
- const mutationVariables = {
- update: expect.anything(),
- context: {
- hasUpload: true,
- },
- mutation: uploadDesignQuery,
- variables: {
- files: [{ name: 'test' }],
- projectPath: '',
- iid: '1',
- },
- optimisticResponse: {
- __typename: 'Mutation',
- designManagementUpload: {
- __typename: 'DesignManagementUploadPayload',
- designs: [
- {
- __typename: 'Design',
- id: expect.anything(),
- image: '',
- imageV432x230: '',
- filename: 'test',
- fullPath: '',
- event: 'NONE',
- notesCount: 0,
- diffRefs: {
- __typename: 'DiffRefs',
- baseSha: '',
- startSha: '',
- headSha: '',
- },
- discussions: {
- __typename: 'DesignDiscussion',
- nodes: [],
- },
- versions: {
- __typename: 'DesignVersionConnection',
- edges: {
- __typename: 'DesignVersionEdge',
- node: {
- __typename: 'DesignVersion',
- id: expect.anything(),
- sha: expect.anything(),
- },
- },
- },
- },
- ],
- skippedDesigns: [],
- errors: [],
- },
- },
- };
-
- return wrapper.vm.$nextTick().then(() => {
- findDropzone().vm.$emit('change', [{ name: 'test' }]);
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
- expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
- expect(wrapper.vm.isSaving).toBeTruthy();
- });
- });
-
- it('sets isSaving', () => {
- createComponent();
-
- const uploadDesign = wrapper.vm.onUploadDesign([
- {
- name: 'test',
- },
- ]);
-
- expect(wrapper.vm.isSaving).toBe(true);
-
- return uploadDesign.then(() => {
- expect(wrapper.vm.isSaving).toBe(false);
- });
- });
-
- it('updates state appropriately after upload complete', () => {
- createComponent({ stubs: { GlEmptyState } });
- wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
-
- wrapper.vm.onUploadDesignDone();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.filesToBeSaved).toEqual([]);
- expect(wrapper.vm.isSaving).toBeFalsy();
- expect(wrapper.vm.isLatestVersion).toBe(true);
- });
- });
-
- it('updates state appropriately after upload error', () => {
- createComponent({ stubs: { GlEmptyState } });
- wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
-
- wrapper.vm.onUploadDesignError();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.filesToBeSaved).toEqual([]);
- expect(wrapper.vm.isSaving).toBeFalsy();
- expect(createFlash).toHaveBeenCalled();
-
- createFlash.mockReset();
- });
- });
-
- it('does not call mutation if createDesign is false', () => {
- createComponent({ createDesign: false });
-
- wrapper.vm.onUploadDesign([]);
-
- expect(mutate).not.toHaveBeenCalled();
- });
-
- describe('upload count limit', () => {
- const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
-
- afterEach(() => {
- createFlash.mockReset();
- });
-
- it('does not warn when the max files are uploaded', () => {
- createComponent();
-
- wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT).fill(mockDesigns[0]));
-
- expect(createFlash).not.toHaveBeenCalled();
- });
-
- it('warns when too many files are uploaded', () => {
- createComponent();
-
- wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT + 1).fill(mockDesigns[0]));
-
- expect(createFlash).toHaveBeenCalled();
- });
- });
-
- it('flashes warning if designs are skipped', () => {
- createComponent({
- mockMutate: () =>
- Promise.resolve({
- data: { designManagementUpload: { skippedDesigns: [{ filename: 'test.jpg' }] } },
- }),
- });
-
- const uploadDesign = wrapper.vm.onUploadDesign([
- {
- name: 'test',
- },
- ]);
-
- return uploadDesign.then(() => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- 'Upload skipped. test.jpg did not change.',
- 'warning',
- );
- });
- });
-
- describe('dragging onto an existing design', () => {
- beforeEach(() => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- });
-
- it('calls onUploadDesign with valid upload', () => {
- wrapper.setMethods({
- onUploadDesign: jest.fn(),
- });
-
- const mockUploadPayload = [
- {
- name: mockDesigns[0].filename,
- },
- ];
-
- const designDropzone = findFirstDropzoneWithDesign();
- designDropzone.vm.$emit('change', mockUploadPayload);
-
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith(mockUploadPayload);
- });
-
- it.each`
- description | eventPayload | message
- ${'> 1 file'} | ${[{ name: 'test' }, { name: 'test-2' }]} | ${EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE}
- ${'different filename'} | ${[{ name: 'wrong-name' }]} | ${EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE}
- `('calls createFlash when upload has $description', ({ eventPayload, message }) => {
- const designDropzone = findFirstDropzoneWithDesign();
- designDropzone.vm.$emit('change', eventPayload);
-
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(message);
- });
- });
- });
-
- describe('on latest version when has designs', () => {
- beforeEach(() => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- });
-
- it('renders design checkboxes', () => {
- expect(findDesignCheckboxes()).toHaveLength(mockDesigns.length);
- });
-
- it('renders toolbar buttons', () => {
- expect(findToolbar().exists()).toBe(true);
- expect(findToolbar().classes()).toContain('d-flex');
- expect(findToolbar().classes()).not.toContain('d-none');
- });
-
- it('adds two designs to selected designs when their checkboxes are checked', () => {
- findDesignCheckboxes()
- .at(0)
- .trigger('click');
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findDesignCheckboxes()
- .at(1)
- .trigger('click');
-
- return wrapper.vm.$nextTick();
- })
- .then(() => {
- expect(findDeleteButton().exists()).toBe(true);
- expect(findSelectAllButton().text()).toBe('Deselect all');
- findDeleteButton().vm.$emit('deleteSelectedDesigns');
- const [{ variables }] = mutate.mock.calls[0];
- expect(variables.filenames).toStrictEqual([
- mockDesigns[0].filename,
- mockDesigns[1].filename,
- ]);
- });
- });
-
- it('adds all designs to selected designs when Select All button is clicked', () => {
- findSelectAllButton().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findDeleteButton().props().hasSelectedDesigns).toBe(true);
- expect(findSelectAllButton().text()).toBe('Deselect all');
- expect(wrapper.vm.selectedDesigns).toEqual(mockDesigns.map(design => design.filename));
- });
- });
-
- it('removes all designs from selected designs when at least one design was selected', () => {
- findDesignCheckboxes()
- .at(0)
- .trigger('click');
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findSelectAllButton().vm.$emit('click');
- })
- .then(() => {
- expect(findDeleteButton().props().hasSelectedDesigns).toBe(false);
- expect(findSelectAllButton().text()).toBe('Select all');
- expect(wrapper.vm.selectedDesigns).toEqual([]);
- });
- });
- });
-
- it('on latest version when has no designs does not render toolbar buttons', () => {
- createComponent({ designs: [], allVersions: [mockVersion] });
- expect(findToolbar().exists()).toBe(false);
- });
-
- describe('on non-latest version', () => {
- beforeEach(() => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
-
- router.replace({
- name: DESIGNS_ROUTE_NAME,
- query: {
- version: '2',
- },
- });
- });
-
- it('does not render design checkboxes', () => {
- expect(findDesignCheckboxes()).toHaveLength(0);
- });
-
- it('does not render Delete selected button', () => {
- expect(findDeleteButton().exists()).toBe(false);
- });
-
- it('does not render Select All button', () => {
- expect(findSelectAllButton().exists()).toBe(false);
- });
- });
-
- describe('pasting a design', () => {
- let event;
- beforeEach(() => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
-
- wrapper.setMethods({
- onUploadDesign: jest.fn(),
- });
-
- event = new Event('paste');
-
- router.replace({
- name: DESIGNS_ROUTE_NAME,
- query: {
- version: '2',
- },
- });
- });
-
- it('calls onUploadDesign with valid paste', () => {
- event.clipboardData = {
- files: [{ name: 'image.png', type: 'image/png' }],
- getData: () => 'test.png',
- };
-
- document.dispatchEvent(event);
-
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
- new File([{ name: 'image.png' }], 'test.png'),
- ]);
- });
-
- it('renames a design if it has an image.png filename', () => {
- event.clipboardData = {
- files: [{ name: 'image.png', type: 'image/png' }],
- getData: () => 'image.png',
- };
-
- document.dispatchEvent(event);
-
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
- new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
- ]);
- });
-
- it('does not call onUploadDesign with invalid paste', () => {
- event.clipboardData = {
- items: [{ type: 'text/plain' }, { type: 'text' }],
- files: [],
- };
-
- document.dispatchEvent(event);
-
- expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
- });
- });
-
- describe('when navigating', () => {
- it('ensures fullscreen layout is not applied', () => {
- createComponent(true);
-
- wrapper.vm.$router.push('/designs');
- expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
- expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/router_spec.js b/spec/frontend/design_management_legacy/router_spec.js
deleted file mode 100644
index 5f62793a243..00000000000
--- a/spec/frontend/design_management_legacy/router_spec.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import VueRouter from 'vue-router';
-import App from '~/design_management_legacy/components/app.vue';
-import Designs from '~/design_management_legacy/pages/index.vue';
-import DesignDetail from '~/design_management_legacy/pages/design/index.vue';
-import createRouter from '~/design_management_legacy/router';
-import {
- ROOT_ROUTE_NAME,
- DESIGNS_ROUTE_NAME,
- DESIGN_ROUTE_NAME,
-} from '~/design_management_legacy/router/constants';
-import '~/commons/bootstrap';
-
-function factory(routeArg) {
- const localVue = createLocalVue();
- localVue.use(VueRouter);
-
- window.gon = { sprite_icons: '' };
-
- const router = createRouter('/');
- if (routeArg !== undefined) {
- router.push(routeArg);
- }
-
- return mount(App, {
- localVue,
- router,
- mocks: {
- $apollo: {
- queries: {
- designs: { loading: true },
- design: { loading: true },
- permissions: { loading: true },
- },
- mutate: jest.fn(),
- },
- },
- });
-}
-
-jest.mock('mousetrap', () => ({
- bind: jest.fn(),
- unbind: jest.fn(),
-}));
-
-describe('Design management router', () => {
- afterEach(() => {
- window.location.hash = '';
- });
-
- describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', routeArg => {
- it('pushes home component', () => {
- const wrapper = factory(routeArg);
-
- expect(wrapper.find(Designs).exists()).toBe(true);
- });
- });
-
- describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', routeArg => {
- it('pushes designs root component', () => {
- const wrapper = factory(routeArg);
-
- expect(wrapper.find(Designs).exists()).toBe(true);
- });
- });
-
- describe.each([['/designs/1'], [{ name: DESIGN_ROUTE_NAME, params: { id: '1' } }]])(
- 'designs detail route',
- routeArg => {
- it('pushes designs detail component', () => {
- const wrapper = factory(routeArg);
-
- return nextTick().then(() => {
- const detail = wrapper.find(DesignDetail);
- expect(detail.exists()).toBe(true);
- expect(detail.props('id')).toEqual('1');
- });
- });
- },
- );
-});
diff --git a/spec/frontend/design_management_legacy/utils/cache_update_spec.js b/spec/frontend/design_management_legacy/utils/cache_update_spec.js
deleted file mode 100644
index dce91b5e59b..00000000000
--- a/spec/frontend/design_management_legacy/utils/cache_update_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import {
- updateStoreAfterDesignsDelete,
- updateStoreAfterAddDiscussionComment,
- updateStoreAfterAddImageDiffNote,
- updateStoreAfterUploadDesign,
- updateStoreAfterUpdateImageDiffNote,
-} from '~/design_management_legacy/utils/cache_update';
-import {
- designDeletionError,
- ADD_DISCUSSION_COMMENT_ERROR,
- ADD_IMAGE_DIFF_NOTE_ERROR,
- UPDATE_IMAGE_DIFF_NOTE_ERROR,
-} from '~/design_management_legacy/utils/error_messages';
-import design from '../mock_data/design';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-
-jest.mock('~/flash.js');
-
-describe('Design Management cache update', () => {
- const mockErrors = ['code red!'];
-
- let mockStore;
-
- beforeEach(() => {
- mockStore = new InMemoryCache();
- });
-
- describe('error handling', () => {
- it.each`
- fnName | subject | errorMessage | extraArgs
- ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
- ${'updateStoreAfterAddDiscussionComment'} | ${updateStoreAfterAddDiscussionComment} | ${ADD_DISCUSSION_COMMENT_ERROR} | ${[]}
- ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
- ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
- ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
- `('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => {
- expect(createFlash).not.toHaveBeenCalled();
- expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow();
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(errorMessage);
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js b/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js
deleted file mode 100644
index 97e85a24a35..00000000000
--- a/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js
+++ /dev/null
@@ -1,176 +0,0 @@
-import {
- extractCurrentDiscussion,
- extractDiscussions,
- findVersionId,
- designUploadOptimisticResponse,
- updateImageDiffNoteOptimisticResponse,
- isValidDesignFile,
- extractDesign,
-} from '~/design_management_legacy/utils/design_management_utils';
-import mockResponseNoDesigns from '../mock_data/no_designs';
-import mockResponseWithDesigns from '../mock_data/designs';
-import mockDesign from '../mock_data/design';
-
-jest.mock('lodash/uniqueId', () => () => 1);
-
-describe('extractCurrentDiscussion', () => {
- let discussions;
-
- beforeEach(() => {
- discussions = {
- nodes: [
- { id: 101, payload: 'w' },
- { id: 102, payload: 'x' },
- { id: 103, payload: 'y' },
- { id: 104, payload: 'z' },
- ],
- };
- });
-
- it('finds the relevant discussion if it exists', () => {
- const id = 103;
- expect(extractCurrentDiscussion(discussions, id)).toEqual({ id, payload: 'y' });
- });
-
- it('returns null if the relevant discussion does not exist', () => {
- expect(extractCurrentDiscussion(discussions, 0)).not.toBeDefined();
- });
-});
-
-describe('extractDiscussions', () => {
- let discussions;
-
- beforeEach(() => {
- discussions = {
- nodes: [
- { id: 1, notes: { nodes: ['a'] } },
- { id: 2, notes: { nodes: ['b'] } },
- { id: 3, notes: { nodes: ['c'] } },
- { id: 4, notes: { nodes: ['d'] } },
- ],
- };
- });
-
- it('discards the edges.node artifacts of GraphQL', () => {
- expect(extractDiscussions(discussions)).toEqual([
- { id: 1, notes: ['a'], index: 1 },
- { id: 2, notes: ['b'], index: 2 },
- { id: 3, notes: ['c'], index: 3 },
- { id: 4, notes: ['d'], index: 4 },
- ]);
- });
-});
-
-describe('version parser', () => {
- it('correctly extracts version ID from a valid version string', () => {
- const testVersionId = '123';
- const testVersionString = `gid://gitlab/DesignManagement::Version/${testVersionId}`;
-
- expect(findVersionId(testVersionString)).toEqual(testVersionId);
- });
-
- it('fails to extract version ID from an invalid version string', () => {
- const testInvalidVersionString = `gid://gitlab/DesignManagement::Version`;
-
- expect(findVersionId(testInvalidVersionString)).toBeUndefined();
- });
-});
-
-describe('optimistic responses', () => {
- it('correctly generated for designManagementUpload', () => {
- const expectedResponse = {
- __typename: 'Mutation',
- designManagementUpload: {
- __typename: 'DesignManagementUploadPayload',
- designs: [
- {
- __typename: 'Design',
- id: -1,
- image: '',
- imageV432x230: '',
- filename: 'test',
- fullPath: '',
- notesCount: 0,
- event: 'NONE',
- diffRefs: { __typename: 'DiffRefs', baseSha: '', startSha: '', headSha: '' },
- discussions: { __typename: 'DesignDiscussion', nodes: [] },
- versions: {
- __typename: 'DesignVersionConnection',
- edges: {
- __typename: 'DesignVersionEdge',
- node: { __typename: 'DesignVersion', id: -1, sha: -1 },
- },
- },
- },
- ],
- errors: [],
- skippedDesigns: [],
- },
- };
- expect(designUploadOptimisticResponse([{ name: 'test' }])).toEqual(expectedResponse);
- });
-
- it('correctly generated for updateImageDiffNoteOptimisticResponse', () => {
- const mockNote = {
- id: 'test-note-id',
- };
-
- const mockPosition = {
- x: 10,
- y: 10,
- width: 10,
- height: 10,
- };
-
- const expectedResponse = {
- __typename: 'Mutation',
- updateImageDiffNote: {
- __typename: 'UpdateImageDiffNotePayload',
- note: {
- ...mockNote,
- position: mockPosition,
- },
- errors: [],
- },
- };
- expect(updateImageDiffNoteOptimisticResponse(mockNote, { position: mockPosition })).toEqual(
- expectedResponse,
- );
- });
-});
-
-describe('isValidDesignFile', () => {
- // test every filetype that Design Management supports
- // https://docs.gitlab.com/ee/user/project/issues/design_management.html#limitations
- it.each`
- mimetype | isValid
- ${'image/svg'} | ${true}
- ${'image/png'} | ${true}
- ${'image/jpg'} | ${true}
- ${'image/jpeg'} | ${true}
- ${'image/gif'} | ${true}
- ${'image/bmp'} | ${true}
- ${'image/tiff'} | ${true}
- ${'image/ico'} | ${true}
- ${'image/svg'} | ${true}
- ${'video/mpeg'} | ${false}
- ${'audio/midi'} | ${false}
- ${'application/octet-stream'} | ${false}
- `('returns $isValid for file type $mimetype', ({ mimetype, isValid }) => {
- expect(isValidDesignFile({ type: mimetype })).toBe(isValid);
- });
-});
-
-describe('extractDesign', () => {
- describe('with no designs', () => {
- it('returns undefined', () => {
- expect(extractDesign(mockResponseNoDesigns)).toBeUndefined();
- });
- });
-
- describe('with designs', () => {
- it('returns the first design available', () => {
- expect(extractDesign(mockResponseWithDesigns)).toEqual(mockDesign);
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/utils/error_messages_spec.js b/spec/frontend/design_management_legacy/utils/error_messages_spec.js
deleted file mode 100644
index 489ac23da4e..00000000000
--- a/spec/frontend/design_management_legacy/utils/error_messages_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import {
- designDeletionError,
- designUploadSkippedWarning,
-} from '~/design_management_legacy/utils/error_messages';
-
-const mockFilenames = n =>
- Array(n)
- .fill(0)
- .map((_, i) => ({ filename: `${i + 1}.jpg` }));
-
-describe('Error message', () => {
- describe('designDeletionError', () => {
- const singularMsg = 'Could not delete a design. Please try again.';
- const pluralMsg = 'Could not delete designs. Please try again.';
-
- describe('when [singular=true]', () => {
- it.each([[undefined], [true]])('uses singular grammar', singularOption => {
- expect(designDeletionError({ singular: singularOption })).toEqual(singularMsg);
- });
- });
-
- describe('when [singular=false]', () => {
- it('uses plural grammar', () => {
- expect(designDeletionError({ singular: false })).toEqual(pluralMsg);
- });
- });
- });
-
- describe.each([
- [[], [], null],
- [mockFilenames(1), mockFilenames(1), 'Upload skipped. 1.jpg did not change.'],
- [
- mockFilenames(2),
- mockFilenames(2),
- 'Upload skipped. The designs you tried uploading did not change.',
- ],
- [
- mockFilenames(2),
- mockFilenames(1),
- 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg.',
- ],
- [
- mockFilenames(6),
- mockFilenames(5),
- 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg.',
- ],
- [
- mockFilenames(7),
- mockFilenames(6),
- 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 1 more.',
- ],
- [
- mockFilenames(8),
- mockFilenames(7),
- 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 2 more.',
- ],
- ])('designUploadSkippedWarning', (uploadedFiles, skippedFiles, expected) => {
- it('returns expected warning message', () => {
- expect(designUploadSkippedWarning(uploadedFiles, skippedFiles)).toBe(expected);
- });
- });
-});
diff --git a/spec/frontend/design_management_legacy/utils/tracking_spec.js b/spec/frontend/design_management_legacy/utils/tracking_spec.js
deleted file mode 100644
index a59cf80c906..00000000000
--- a/spec/frontend/design_management_legacy/utils/tracking_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { mockTracking } from 'helpers/tracking_helper';
-import { trackDesignDetailView } from '~/design_management_legacy/utils/tracking';
-
-function getTrackingSpy(key) {
- return mockTracking(key, undefined, jest.spyOn);
-}
-
-describe('Tracking Events', () => {
- describe('trackDesignDetailView', () => {
- const eventKey = 'projects:issues:design';
- const eventName = 'view_design';
-
- it('trackDesignDetailView fires a tracking event when called', () => {
- const trackingSpy = getTrackingSpy(eventKey);
-
- trackDesignDetailView();
-
- expect(trackingSpy).toHaveBeenCalledWith(
- eventKey,
- eventName,
- expect.objectContaining({
- label: eventName,
- context: {
- schema: expect.any(String),
- data: {
- 'design-version-number': 1,
- 'design-is-current-version': false,
- 'internal-object-referrer': '',
- 'design-collection-owner': '',
- },
- },
- }),
- );
- });
-
- it('trackDesignDetailView allows to customize the value payload', () => {
- const trackingSpy = getTrackingSpy(eventKey);
-
- trackDesignDetailView('from-a-test', 'test', 100, true);
-
- expect(trackingSpy).toHaveBeenCalledWith(
- eventKey,
- eventName,
- expect.objectContaining({
- label: eventName,
- context: {
- schema: expect.any(String),
- data: {
- 'design-version-number': 100,
- 'design-is-current-version': true,
- 'internal-object-referrer': 'from-a-test',
- 'design-collection-owner': 'test',
- },
- },
- }),
- );
- });
- });
-});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index ac046ddc203..cd3a6aa0e28 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -1,6 +1,6 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
import Mousetrap from 'mousetrap';
@@ -9,6 +9,7 @@ import NoChanges from '~/diffs/components/no_changes.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
import CompareVersions from '~/diffs/components/compare_versions.vue';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
+import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
import TreeList from '~/diffs/components/tree_list.vue';
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
@@ -22,6 +23,10 @@ const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
const COMMIT_URL = '[BASE URL]/OLD';
const UPDATED_COMMIT_URL = '[BASE URL]/NEW';
+function getCollapsedFilesWarning(wrapper) {
+ return wrapper.find(CollapsedFilesWarning);
+}
+
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
let store;
@@ -108,7 +113,6 @@ describe('diffs/components/app', () => {
};
jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn());
createComponent();
- jest.spyOn(wrapper.vm, 'fetchDiffFiles').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'fetchDiffFilesMeta').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver);
jest.spyOn(wrapper.vm, 'fetchCoverageFiles').mockImplementation(fetchResolver);
@@ -139,37 +143,21 @@ describe('diffs/components/app', () => {
parallel_diff_lines: ['line'],
};
- function expectFetchToOccur({
- vueInstance,
- done = () => {},
- batch = false,
- existingFiles = 1,
- } = {}) {
+ function expectFetchToOccur({ vueInstance, done = () => {}, existingFiles = 1 } = {}) {
vueInstance.$nextTick(() => {
expect(vueInstance.diffFiles.length).toEqual(existingFiles);
-
- if (!batch) {
- expect(vueInstance.fetchDiffFiles).toHaveBeenCalled();
- expect(vueInstance.fetchDiffFilesBatch).not.toHaveBeenCalled();
- } else {
- expect(vueInstance.fetchDiffFiles).not.toHaveBeenCalled();
- expect(vueInstance.fetchDiffFilesBatch).toHaveBeenCalled();
- }
+ expect(vueInstance.fetchDiffFilesBatch).toHaveBeenCalled();
done();
});
}
- beforeEach(() => {
- wrapper.vm.glFeatures.singleMrDiffView = true;
- });
-
it('fetches diffs if it has none', done => {
wrapper.vm.isLatestVersion = () => false;
store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
- expectFetchToOccur({ vueInstance: wrapper.vm, batch: false, existingFiles: 0, done });
+ expectFetchToOccur({ vueInstance: wrapper.vm, existingFiles: 0, done });
});
it('fetches diffs if it has both view styles, but no lines in either', done => {
@@ -200,89 +188,46 @@ describe('diffs/components/app', () => {
});
it('fetches batch diffs if it has none', done => {
- wrapper.vm.glFeatures.diffsBatchLoad = true;
-
store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
- expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, existingFiles: 0, done });
+ expectFetchToOccur({ vueInstance: wrapper.vm, existingFiles: 0, done });
});
it('fetches batch diffs if it has both view styles, but no lines in either', done => {
- wrapper.vm.glFeatures.diffsBatchLoad = true;
-
store.state.diffs.diffFiles.push(noLinesDiff);
store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
- expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done });
+ expectFetchToOccur({ vueInstance: wrapper.vm, done });
});
it('fetches batch diffs if it only has inline view style', done => {
- wrapper.vm.glFeatures.diffsBatchLoad = true;
-
store.state.diffs.diffFiles.push(inlineLinesDiff);
store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
- expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done });
+ expectFetchToOccur({ vueInstance: wrapper.vm, done });
});
it('fetches batch diffs if it only has parallel view style', done => {
- wrapper.vm.glFeatures.diffsBatchLoad = true;
-
store.state.diffs.diffFiles.push(parallelLinesDiff);
store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
- expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done });
- });
-
- it('does not fetch diffs if it has already fetched both styles of diff', () => {
- wrapper.vm.glFeatures.diffsBatchLoad = false;
-
- store.state.diffs.diffFiles.push(fullDiff);
- store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
-
- expect(wrapper.vm.diffFiles.length).toEqual(1);
- expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
- expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
+ expectFetchToOccur({ vueInstance: wrapper.vm, done });
});
it('does not fetch batch diffs if it has already fetched both styles of diff', () => {
- wrapper.vm.glFeatures.diffsBatchLoad = true;
-
store.state.diffs.diffFiles.push(fullDiff);
store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
expect(wrapper.vm.diffFiles.length).toEqual(1);
- expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
});
});
- it('calls fetchDiffFiles if diffsBatchLoad is not enabled', done => {
- expect(wrapper.vm.diffFilesLength).toEqual(0);
- wrapper.vm.glFeatures.diffsBatchLoad = false;
- wrapper.vm.fetchData(false);
-
- expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled();
- setImmediate(() => {
- expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
- expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled();
- expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
- expect(wrapper.vm.fetchCoverageFiles).toHaveBeenCalled();
- expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
- expect(wrapper.vm.diffFilesLength).toEqual(100);
- expect(wrapper.vm.unwatchRetrievingBatches).toHaveBeenCalled();
-
- done();
- });
- });
-
it('calls batch methods if diffsBatchLoad is enabled, and not latest version', done => {
expect(wrapper.vm.diffFilesLength).toEqual(0);
- wrapper.vm.glFeatures.diffsBatchLoad = true;
wrapper.vm.isLatestVersion = () => false;
wrapper.vm.fetchData(false);
- expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
setImmediate(() => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
@@ -297,10 +242,8 @@ describe('diffs/components/app', () => {
it('calls batch methods if diffsBatchLoad is enabled, and latest version', done => {
expect(wrapper.vm.diffFilesLength).toEqual(0);
- wrapper.vm.glFeatures.diffsBatchLoad = true;
wrapper.vm.fetchData(false);
- expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
setImmediate(() => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
@@ -320,7 +263,7 @@ describe('diffs/components/app', () => {
state.diffs.isParallelView = false;
});
- expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true);
+ expect(wrapper.find('.container-limited.limit-container-width').exists()).toBe(true);
});
it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
@@ -329,7 +272,7 @@ describe('diffs/components/app', () => {
state.diffs.isParallelView = false;
});
- expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false);
+ expect(wrapper.find('.container-limited.limit-container-width').exists()).toBe(false);
});
it('does not add container-limiting classes when isFluidLayout', () => {
@@ -337,7 +280,7 @@ describe('diffs/components/app', () => {
state.diffs.isParallelView = false;
});
- expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false);
+ expect(wrapper.find('.container-limited.limit-container-width').exists()).toBe(false);
});
it('displays loading icon on loading', () => {
@@ -345,7 +288,7 @@ describe('diffs/components/app', () => {
state.diffs.isLoading = true;
});
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('displays loading icon on batch loading', () => {
@@ -353,20 +296,20 @@ describe('diffs/components/app', () => {
state.diffs.isBatchLoading = true;
});
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('displays diffs container when not loading', () => {
createComponent();
- expect(wrapper.contains(GlLoadingIcon)).toBe(false);
- expect(wrapper.contains('#diffs')).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('#diffs').exists()).toBe(true);
});
it('does not show commit info', () => {
createComponent();
- expect(wrapper.contains('.blob-commit-info')).toBe(false);
+ expect(wrapper.find('.blob-commit-info').exists()).toBe(false);
});
describe('row highlighting', () => {
@@ -442,7 +385,7 @@ describe('diffs/components/app', () => {
it('renders empty state when no diff files exist', () => {
createComponent();
- expect(wrapper.contains(NoChanges)).toBe(true);
+ expect(wrapper.find(NoChanges).exists()).toBe(true);
});
it('does not render empty state when diff files exist', () => {
@@ -452,7 +395,7 @@ describe('diffs/components/app', () => {
});
});
- expect(wrapper.contains(NoChanges)).toBe(false);
+ expect(wrapper.find(NoChanges).exists()).toBe(false);
expect(wrapper.findAll(DiffFile).length).toBe(1);
});
@@ -462,7 +405,7 @@ describe('diffs/components/app', () => {
state.diffs.mergeRequestDiff = mergeRequestDiff;
});
- expect(wrapper.contains(NoChanges)).toBe(false);
+ expect(wrapper.find(NoChanges).exists()).toBe(false);
});
});
@@ -722,7 +665,7 @@ describe('diffs/components/app', () => {
state.diffs.mergeRequestDiff = mergeRequestDiff;
});
- expect(wrapper.contains(CompareVersions)).toBe(true);
+ expect(wrapper.find(CompareVersions).exists()).toBe(true);
expect(wrapper.find(CompareVersions).props()).toEqual(
expect.objectContaining({
mergeRequestDiffs: diffsMockData,
@@ -730,24 +673,51 @@ describe('diffs/components/app', () => {
);
});
- it('should render hidden files warning if render overflow warning is present', () => {
- createComponent({}, ({ state }) => {
- state.diffs.renderOverflowWarning = true;
- state.diffs.realSize = '5';
- state.diffs.plainDiffPath = 'plain diff path';
- state.diffs.emailPatchPath = 'email patch path';
- state.diffs.size = 1;
+ describe('warnings', () => {
+ describe('hidden files', () => {
+ it('should render hidden files warning if render overflow warning is present', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.renderOverflowWarning = true;
+ state.diffs.realSize = '5';
+ state.diffs.plainDiffPath = 'plain diff path';
+ state.diffs.emailPatchPath = 'email patch path';
+ state.diffs.size = 1;
+ });
+
+ expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true);
+ expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
+ expect.objectContaining({
+ total: '5',
+ plainDiffPath: 'plain diff path',
+ emailPatchPath: 'email patch path',
+ visible: 1,
+ }),
+ );
+ });
});
- expect(wrapper.contains(HiddenFilesWarning)).toBe(true);
- expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
- expect.objectContaining({
- total: '5',
- plainDiffPath: 'plain diff path',
- emailPatchPath: 'email patch path',
- visible: 1,
- }),
- );
+ describe('collapsed files', () => {
+ it('should render the collapsed files warning if there are any collapsed files', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.diffFiles = [{ viewer: { collapsed: true } }];
+ });
+
+ expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
+ });
+
+ it('should not render the collapsed files warning if the user has dismissed the alert already', async () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.diffFiles = [{ viewer: { collapsed: true } }];
+ });
+
+ expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
+
+ wrapper.vm.collapsedWarningDismissed = true;
+ await wrapper.vm.$nextTick();
+
+ expect(getCollapsedFilesWarning(wrapper).exists()).toBe(false);
+ });
+ });
});
it('should display commit widget if store has a commit', () => {
@@ -757,7 +727,7 @@ describe('diffs/components/app', () => {
};
});
- expect(wrapper.contains(CommitWidget)).toBe(true);
+ expect(wrapper.find(CommitWidget).exists()).toBe(true);
});
it('should display diff file if there are diff files', () => {
@@ -765,7 +735,7 @@ describe('diffs/components/app', () => {
state.diffs.diffFiles.push({ sha: '123' });
});
- expect(wrapper.contains(DiffFile)).toBe(true);
+ expect(wrapper.find(DiffFile).exists()).toBe(true);
});
it('should render tree list', () => {
@@ -843,13 +813,16 @@ describe('diffs/components/app', () => {
});
describe('pagination', () => {
+ const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]');
+ const paginator = () => fileByFileNav().find(GlPagination);
+
it('sets previous button as disabled', () => {
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
});
- expect(wrapper.find('[data-testid="singleFilePrevious"]').props('disabled')).toBe(true);
- expect(wrapper.find('[data-testid="singleFileNext"]').props('disabled')).toBe(false);
+ expect(paginator().attributes('prevpage')).toBe(undefined);
+ expect(paginator().attributes('nextpage')).toBe('2');
});
it('sets next button as disabled', () => {
@@ -858,17 +831,26 @@ describe('diffs/components/app', () => {
state.diffs.currentDiffFileId = '312';
});
- expect(wrapper.find('[data-testid="singleFilePrevious"]').props('disabled')).toBe(false);
- expect(wrapper.find('[data-testid="singleFileNext"]').props('disabled')).toBe(true);
+ expect(paginator().attributes('prevpage')).toBe('1');
+ expect(paginator().attributes('nextpage')).toBe(undefined);
+ });
+
+ it("doesn't display when there's fewer than 2 files", () => {
+ createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
+ state.diffs.diffFiles.push({ file_hash: '123' });
+ state.diffs.currentDiffFileId = '123';
+ });
+
+ expect(fileByFileNav().exists()).toBe(false);
});
it.each`
- currentDiffFileId | button | index
- ${'123'} | ${'singleFileNext'} | ${1}
- ${'312'} | ${'singleFilePrevious'} | ${0}
+ currentDiffFileId | targetFile
+ ${'123'} | ${2}
+ ${'312'} | ${1}
`(
- 'it calls navigateToDiffFileIndex with $index when $button is clicked',
- ({ currentDiffFileId, button, index }) => {
+ 'it calls navigateToDiffFileIndex with $index when $link is clicked',
+ async ({ currentDiffFileId, targetFile }) => {
createComponent({ viewDiffsFileByFile: true }, ({ state }) => {
state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
state.diffs.currentDiffFileId = currentDiffFileId;
@@ -876,11 +858,11 @@ describe('diffs/components/app', () => {
jest.spyOn(wrapper.vm, 'navigateToDiffFileIndex');
- wrapper.find(`[data-testid="${button}"]`).vm.$emit('click');
+ paginator().vm.$emit('input', targetFile);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(index);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(targetFile - 1);
},
);
});
diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
new file mode 100644
index 00000000000..670eab5472f
--- /dev/null
+++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
@@ -0,0 +1,88 @@
+import Vuex from 'vuex';
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import createStore from '~/diffs/store/modules';
+import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
+import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
+
+const propsData = {
+ limited: true,
+ mergeable: true,
+ resolutionPath: 'a-path',
+};
+const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
+
+describe('CollapsedFilesWarning', () => {
+ const localVue = createLocalVue();
+ let store;
+ let wrapper;
+
+ localVue.use(Vuex);
+
+ const getAlertActionButton = () =>
+ wrapper.find(CollapsedFilesWarning).find('button.gl-alert-action:first-child');
+ const getAlertCloseButton = () => wrapper.find(CollapsedFilesWarning).find('button');
+
+ const createComponent = (props = {}, { full } = { full: false }) => {
+ const mounter = full ? mount : shallowMount;
+ store = new Vuex.Store({
+ modules: {
+ diffs: createStore(),
+ },
+ });
+
+ wrapper = mounter(CollapsedFilesWarning, {
+ propsData: { ...propsData, ...props },
+ localVue,
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ limited | containerClasses
+ ${true} | ${limitedClasses}
+ ${false} | ${[]}
+ `(
+ 'has the correct container classes when limited is $limited',
+ ({ limited, containerClasses }) => {
+ createComponent({ limited });
+
+ expect(wrapper.classes()).toEqual(containerClasses);
+ },
+ );
+
+ it.each`
+ present | dismissed
+ ${false} | ${true}
+ ${true} | ${false}
+ `('toggles the alert when dismissed is $dismissed', ({ present, dismissed }) => {
+ createComponent({ dismissed });
+
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(present);
+ });
+
+ it('dismisses the component when the alert "x" is clicked', async () => {
+ createComponent({}, { full: true });
+
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
+
+ getAlertCloseButton().element.click();
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
+ });
+
+ it('triggers the expandAllFiles action when the alert action button is clicked', () => {
+ createComponent({}, { full: true });
+
+ jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
+
+ getAlertActionButton().vm.$emit('click');
+
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/expandAllFiles', undefined);
+ });
+});
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 0df951d43a7..c48445790f7 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -24,8 +24,7 @@ describe('diffs/components/commit_item', () => {
const getTitleElement = () => wrapper.find('.commit-row-message.item-title');
const getDescElement = () => wrapper.find('pre.commit-row-description');
- const getDescExpandElement = () =>
- wrapper.find('.commit-content .text-expander.js-toggle-button');
+ const getDescExpandElement = () => wrapper.find('.commit-content .js-toggle-button');
const getShaElement = () => wrapper.find('.commit-sha-group');
const getAvatarElement = () => wrapper.find('.user-avatar-link');
const getCommitterElement = () => wrapper.find('.committer');
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 7fdbc791589..b3dfc71260c 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -2,7 +2,6 @@ import { trimText } from 'helpers/text_helper';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import { createStore } from '~/mr_notes/stores';
import diffsMockData from '../mock_data/merge_request_diffs';
import getDiffWithCommit from '../mock_data/diff_with_commit';
@@ -51,7 +50,7 @@ describe('CompareVersions', () => {
expect(treeListBtn.exists()).toBe(true);
expect(treeListBtn.attributes('title')).toBe('Hide file browser');
- expect(treeListBtn.find(Icon).props('name')).toBe('file-tree');
+ expect(treeListBtn.props('icon')).toBe('file-tree');
});
it('should render comparison dropdowns with correct values', () => {
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index b78895f9e55..6d0120d888e 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -177,23 +177,19 @@ describe('DiffContent', () => {
});
wrapper.find(NoteForm).vm.$emit('handleFormUpdate', noteStub);
- expect(saveDiffDiscussionMock).toHaveBeenCalledWith(
- expect.any(Object),
- {
- note: noteStub,
- formData: {
- noteableData: expect.any(Object),
- diffFile: currentDiffFile,
- positionType: IMAGE_DIFF_POSITION_TYPE,
- x: undefined,
- y: undefined,
- width: undefined,
- height: undefined,
- noteableType: undefined,
- },
+ expect(saveDiffDiscussionMock).toHaveBeenCalledWith(expect.any(Object), {
+ note: noteStub,
+ formData: {
+ noteableData: expect.any(Object),
+ diffFile: currentDiffFile,
+ positionType: IMAGE_DIFF_POSITION_TYPE,
+ x: undefined,
+ y: undefined,
+ width: undefined,
+ height: undefined,
+ noteableType: undefined,
},
- undefined,
- );
+ });
});
});
});
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 83becc7a20a..96b76183cee 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -1,9 +1,9 @@
import { mount, createLocalVue } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import { createStore } from '~/mr_notes/stores';
import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
@@ -51,7 +51,7 @@ describe('DiffDiscussions', () => {
const diffNotesToggle = findDiffNotesToggle();
expect(diffNotesToggle.exists()).toBe(true);
- expect(diffNotesToggle.find(Icon).exists()).toBe(true);
+ expect(diffNotesToggle.find(GlIcon).exists()).toBe(true);
expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true);
});
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index b8aca4ad86b..81e08f09f62 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -10,7 +10,6 @@ import diffFileMockData from '../mock_data/diff_file';
const EXPAND_UP_CLASS = '.js-unfold';
const EXPAND_DOWN_CLASS = '.js-unfold-down';
-const LINE_TO_USE = 5;
const lineSources = {
[INLINE_DIFF_VIEW_TYPE]: 'highlighted_diff_lines',
[PARALLEL_DIFF_VIEW_TYPE]: 'parallel_diff_lines',
@@ -66,7 +65,7 @@ describe('DiffExpansionCell', () => {
beforeEach(() => {
mockFile = cloneDeep(diffFileMockData);
- mockLine = getLine(mockFile, INLINE_DIFF_VIEW_TYPE, LINE_TO_USE);
+ mockLine = getLine(mockFile, INLINE_DIFF_VIEW_TYPE, 8);
store = createStore();
store.state.diffs.diffFiles = [mockFile];
jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve());
@@ -88,7 +87,7 @@ describe('DiffExpansionCell', () => {
const findExpandUp = () => vm.$el.querySelector(EXPAND_UP_CLASS);
const findExpandDown = () => vm.$el.querySelector(EXPAND_DOWN_CLASS);
- const findExpandAll = () => getByText(vm.$el, 'Show unchanged lines');
+ const findExpandAll = () => getByText(vm.$el, 'Show all unchanged lines');
describe('top row', () => {
it('should have "expand up" and "show all" option', () => {
@@ -126,12 +125,12 @@ describe('DiffExpansionCell', () => {
describe('any row', () => {
[
- { diffViewType: INLINE_DIFF_VIEW_TYPE, file: { parallel_diff_lines: [] } },
- { diffViewType: PARALLEL_DIFF_VIEW_TYPE, file: { highlighted_diff_lines: [] } },
- ].forEach(({ diffViewType, file }) => {
+ { diffViewType: INLINE_DIFF_VIEW_TYPE, lineIndex: 8, file: { parallel_diff_lines: [] } },
+ { diffViewType: PARALLEL_DIFF_VIEW_TYPE, lineIndex: 7, file: { highlighted_diff_lines: [] } },
+ ].forEach(({ diffViewType, file, lineIndex }) => {
describe(`with diffViewType (${diffViewType})`, () => {
beforeEach(() => {
- mockLine = getLine(mockFile, diffViewType, LINE_TO_USE);
+ mockLine = getLine(mockFile, diffViewType, lineIndex);
store.state.diffs.diffFiles = [{ ...mockFile, ...file }];
store.state.diffs.diffViewType = diffViewType;
});
@@ -189,10 +188,10 @@ describe('DiffExpansionCell', () => {
});
it('on expand down clicked, dispatch loadMoreLines', () => {
- mockFile[lineSources[diffViewType]][LINE_TO_USE + 1] = cloneDeep(
- mockFile[lineSources[diffViewType]][LINE_TO_USE],
+ mockFile[lineSources[diffViewType]][lineIndex + 1] = cloneDeep(
+ mockFile[lineSources[diffViewType]][lineIndex],
);
- const nextLine = getLine(mockFile, diffViewType, LINE_TO_USE + 1);
+ const nextLine = getLine(mockFile, diffViewType, lineIndex + 1);
nextLine.meta_data.old_pos = 300;
nextLine.meta_data.new_pos = 300;
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index 671dced080c..a0cad32b9fb 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -1,9 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import { GlIcon } from '@gitlab/ui';
+import { cloneDeep } from 'lodash';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import EditButton from '~/diffs/components/edit_button.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import diffDiscussionsMockData from '../mock_data/diff_discussions';
import { truncateSha } from '~/lib/utils/text_utility';
import { diffViewerModes } from '~/ide/constants';
@@ -26,12 +27,16 @@ const diffFile = Object.freeze(
}),
);
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('DiffFileHeader component', () => {
let wrapper;
+ let mockStoreConfig;
const diffHasExpandedDiscussionsResultMock = jest.fn();
const diffHasDiscussionsResultMock = jest.fn();
- const mockStoreConfig = {
+ const defaultMockStoreConfig = {
state: {},
modules: {
diffs: {
@@ -44,6 +49,7 @@ describe('DiffFileHeader component', () => {
toggleFileDiscussions: jest.fn(),
toggleFileDiscussionWrappers: jest.fn(),
toggleFullDiff: jest.fn(),
+ toggleActiveFileByHash: jest.fn(),
},
},
},
@@ -55,6 +61,8 @@ describe('DiffFileHeader component', () => {
diffHasExpandedDiscussionsResultMock,
...Object.values(mockStoreConfig.modules.diffs.actions),
].forEach(mock => mock.mockReset());
+
+ wrapper.destroy();
});
const findHeader = () => wrapper.find({ ref: 'header' });
@@ -70,7 +78,7 @@ describe('DiffFileHeader component', () => {
const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
const findIconByName = iconName => {
- const icons = wrapper.findAll(Icon).filter(w => w.props('name') === iconName);
+ const icons = wrapper.findAll(GlIcon).filter(w => w.props('name') === iconName);
if (icons.length === 0) return icons;
if (icons.length > 1) {
throw new Error(`Multiple icons found for ${iconName}`);
@@ -79,8 +87,7 @@ describe('DiffFileHeader component', () => {
};
const createComponent = props => {
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ mockStoreConfig = cloneDeep(defaultMockStoreConfig);
const store = new Vuex.Store(mockStoreConfig);
wrapper = shallowMount(DiffFileHeader, {
@@ -285,7 +292,7 @@ describe('DiffFileHeader component', () => {
findToggleDiscussionsButton().vm.$emit('click');
expect(
mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers,
- ).toHaveBeenCalledWith(expect.any(Object), diffFile, undefined);
+ ).toHaveBeenCalledWith(expect.any(Object), diffFile);
});
});
diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js
index afdd4bfb335..23adc8f9da4 100644
--- a/spec/frontend/diffs/components/diff_file_row_spec.js
+++ b/spec/frontend/diffs/components/diff_file_row_spec.js
@@ -7,9 +7,12 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
describe('Diff File Row component', () => {
let wrapper;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, highlightCurrentDiffRow = false) => {
wrapper = shallowMount(DiffFileRow, {
propsData: { ...props },
+ provide: {
+ glFeatures: { highlightCurrentDiffRow },
+ },
});
};
@@ -56,6 +59,31 @@ describe('Diff File Row component', () => {
);
});
+ it.each`
+ features | fileType | isViewed | expected
+ ${{ highlightCurrentDiffRow: true }} | ${'blob'} | ${false} | ${'gl-font-weight-bold'}
+ ${{}} | ${'blob'} | ${true} | ${''}
+ ${{}} | ${'tree'} | ${false} | ${''}
+ ${{}} | ${'tree'} | ${true} | ${''}
+ `(
+ 'with (features="$features", fileType="$fileType", isViewed=$isViewed), sets fileClasses="$expected"',
+ ({ features, fileType, isViewed, expected }) => {
+ createComponent(
+ {
+ file: {
+ type: fileType,
+ fileHash: '#123456789',
+ },
+ level: 0,
+ hideFileStats: false,
+ viewedFiles: isViewed ? { '#123456789': true } : {},
+ },
+ features.highlightCurrentDiffRow,
+ );
+ expect(wrapper.find(FileRow).props('fileClasses')).toBe(expected);
+ },
+ );
+
describe('FileRowStats components', () => {
it.each`
type | hideFileStats | value | desc
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index ead8bd79cdb..79f0f6bc327 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -45,7 +45,7 @@ describe('DiffFile', () => {
vm.$nextTick()
.then(() => {
- expect(el.querySelectorAll('.line_content').length).toBe(5);
+ expect(el.querySelectorAll('.line_content').length).toBe(8);
expect(el.querySelectorAll('.js-line-expansion-content').length).toBe(1);
triggerEvent('.btn-clipboard');
})
@@ -90,8 +90,8 @@ describe('DiffFile', () => {
vm.isCollapsed = true;
vm.$nextTick(() => {
- expect(vm.$el.innerText).toContain('This diff is collapsed');
- expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
+ expect(vm.$el.innerText).toContain('This file is collapsed.');
+ expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
done();
});
@@ -102,8 +102,8 @@ describe('DiffFile', () => {
vm.isCollapsed = true;
vm.$nextTick(() => {
- expect(vm.$el.innerText).toContain('This diff is collapsed');
- expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
+ expect(vm.$el.innerText).toContain('This file is collapsed.');
+ expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
done();
});
@@ -121,28 +121,8 @@ describe('DiffFile', () => {
vm.isCollapsed = true;
vm.$nextTick(() => {
- expect(vm.$el.innerText).toContain('This diff is collapsed');
- expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
-
- done();
- });
- });
-
- it('should auto-expand collapsed files when viewDiffsFileByFile is true', done => {
- vm.$destroy();
- window.gon = {
- features: { autoExpandCollapsedDiffs: true },
- };
- vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
- file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
- canCurrentUserFork: false,
- viewDiffsFileByFile: true,
- }).$mount();
-
- vm.$nextTick(() => {
- expect(vm.$el.innerText).not.toContain('This diff is collapsed');
-
- window.gon = {};
+ expect(vm.$el.innerText).toContain('This file is collapsed.');
+ expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
done();
});
@@ -155,7 +135,7 @@ describe('DiffFile', () => {
vm.file.viewer.name = diffViewerModes.renamed;
vm.$nextTick(() => {
- expect(vm.$el.innerText).not.toContain('This diff is collapsed');
+ expect(vm.$el.innerText).not.toContain('This file is collapsed.');
done();
});
@@ -168,7 +148,7 @@ describe('DiffFile', () => {
vm.file.viewer.name = diffViewerModes.mode_changed;
vm.$nextTick(() => {
- expect(vm.$el.innerText).not.toContain('This diff is collapsed');
+ expect(vm.$el.innerText).not.toContain('This file is collapsed.');
done();
});
@@ -235,7 +215,7 @@ describe('DiffFile', () => {
it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => {
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
- vm.file.highlighted_diff_lines = undefined;
+ vm.file.highlighted_diff_lines = [];
vm.file.parallel_diff_lines = [];
vm.isCollapsed = true;
@@ -262,8 +242,8 @@ describe('DiffFile', () => {
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
- vm.file.highlighted_diff_lines = undefined;
- vm.file.parallel_diff_lines = [];
+ vm.file.highlighted_diff_lines = [];
+ vm.file.parallel_diff_lines = undefined;
vm.isCollapsed = true;
vm.$nextTick()
diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js
index 7a083fb6bde..4dcbb3ec332 100644
--- a/spec/frontend/diffs/components/diff_stats_spec.js
+++ b/spec/frontend/diffs/components/diff_stats_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import DiffStats from '~/diffs/components/diff_stats.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const TEST_ADDED_LINES = 100;
const TEST_REMOVED_LINES = 200;
@@ -53,7 +53,7 @@ describe('diff_stats', () => {
describe('files changes', () => {
const findIcon = name =>
wrapper
- .findAll(Icon)
+ .findAll(GlIcon)
.filter(c => c.attributes('name') === name)
.at(0).element.parentNode;
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index accf0a972d0..5a88a3cabd1 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { createStore } from '~/mr_notes/stores';
import { imageDiffDiscussions } from '../mock_data/diff_discussions';
-import Icon from '~/vue_shared/components/icon.vue';
describe('Diffs image diff overlay component', () => {
const dimensions = {
@@ -64,7 +64,7 @@ describe('Diffs image diff overlay component', () => {
it('renders icon when showCommentIcon is true', () => {
createComponent({ showCommentIcon: true });
- expect(wrapper.find(Icon).exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
});
it('sets badge comment positions', () => {
diff --git a/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js
index 90f012fbafe..81e5403d502 100644
--- a/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js
@@ -5,12 +5,13 @@ import InlineDiffExpansionRow from '~/diffs/components/inline_diff_expansion_row
import diffFileMockData from '../mock_data/diff_file';
describe('InlineDiffExpansionRow', () => {
- const matchLine = diffFileMockData.highlighted_diff_lines[5];
+ const mockData = { ...diffFileMockData };
+ const matchLine = mockData.highlighted_diff_lines.pop();
const createComponent = (options = {}) => {
const cmp = Vue.extend(InlineDiffExpansionRow);
const defaults = {
- fileHash: diffFileMockData.file_hash,
+ fileHash: mockData.file_hash,
contextLinesPath: 'contextLinesPath',
line: matchLine,
isTop: false,
diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
index f929f97b598..951b3f6258b 100644
--- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
@@ -1,114 +1,317 @@
import { shallowMount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
import { createStore } from '~/mr_notes/stores';
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
+import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import diffFileMockData from '../mock_data/diff_file';
+import discussionsMockData from '../mock_data/diff_discussions';
+
+const TEST_USER_ID = 'abc123';
+const TEST_USER = { id: TEST_USER_ID };
describe('InlineDiffTableRow', () => {
let wrapper;
- let vm;
+ let store;
const thisLine = diffFileMockData.highlighted_diff_lines[0];
- beforeEach(() => {
+ const createComponent = (props = {}, propsStore = store) => {
wrapper = shallowMount(InlineDiffTableRow, {
- store: createStore(),
+ store: propsStore,
propsData: {
line: thisLine,
fileHash: diffFileMockData.file_hash,
filePath: diffFileMockData.file_path,
contextLinesPath: 'contextLinesPath',
isHighlighted: false,
+ ...props,
},
});
- vm = wrapper.vm;
+ };
+
+ const setWindowLocation = value => {
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.notes.userData = TEST_USER;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
});
- it('does not add hll class to line content when line does not match highlighted row', done => {
- vm.$nextTick()
- .then(() => {
- expect(wrapper.find('.line_content').classes('hll')).toBe(false);
- })
- .then(done)
- .catch(done.fail);
+ it('does not add hll class to line content when line does not match highlighted row', () => {
+ createComponent();
+ expect(wrapper.find('.line_content').classes('hll')).toBe(false);
});
- it('adds hll class to lineContent when line is the highlighted row', done => {
- vm.$nextTick()
- .then(() => {
- vm.$store.state.diffs.highlightedRow = thisLine.line_code;
-
- return vm.$nextTick();
- })
- .then(() => {
- expect(wrapper.find('.line_content').classes('hll')).toBe(true);
- })
- .then(done)
- .catch(done.fail);
+ it('adds hll class to lineContent when line is the highlighted row', () => {
+ store.state.diffs.highlightedRow = thisLine.line_code;
+ createComponent({}, store);
+ expect(wrapper.find('.line_content').classes('hll')).toBe(true);
});
it('adds hll class to lineContent when line is part of a multiline comment', () => {
- wrapper.setProps({ isCommented: true });
- return vm.$nextTick().then(() => {
- expect(wrapper.find('.line_content').classes('hll')).toBe(true);
- });
+ createComponent({ isCommented: true });
+ expect(wrapper.find('.line_content').classes('hll')).toBe(true);
});
describe('sets coverage title and class', () => {
- it('for lines with coverage', done => {
- vm.$nextTick()
- .then(() => {
- const name = diffFileMockData.file_path;
- const line = thisLine.new_line;
-
- vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } };
-
- return vm.$nextTick();
- })
- .then(() => {
- const coverage = wrapper.find('.line-coverage');
-
- expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
- expect(coverage.classes('coverage')).toBe(true);
- })
- .then(done)
- .catch(done.fail);
+ it('for lines with coverage', () => {
+ const name = diffFileMockData.file_path;
+ const line = thisLine.new_line;
+
+ store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } };
+ createComponent({}, store);
+ const coverage = wrapper.find('.line-coverage');
+
+ expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
+ expect(coverage.classes('coverage')).toBe(true);
+ });
+
+ it('for lines without coverage', () => {
+ const name = diffFileMockData.file_path;
+ const line = thisLine.new_line;
+
+ store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } };
+ createComponent({}, store);
+ const coverage = wrapper.find('.line-coverage');
+
+ expect(coverage.attributes('title')).toContain('No test coverage');
+ expect(coverage.classes('no-coverage')).toBe(true);
+ });
+
+ it('for unknown lines', () => {
+ store.state.diffs.coverageFiles = {};
+ createComponent({}, store);
+
+ const coverage = wrapper.find('.line-coverage');
+
+ expect(coverage.attributes('title')).toBeUndefined();
+ expect(coverage.classes('coverage')).toBe(false);
+ expect(coverage.classes('no-coverage')).toBe(false);
+ });
+ });
+
+ describe('Table Cells', () => {
+ const findNewTd = () => wrapper.find({ ref: 'newTd' });
+ const findOldTd = () => wrapper.find({ ref: 'oldTd' });
+
+ describe('td', () => {
+ it('highlights when isHighlighted true', () => {
+ store.state.diffs.highlightedRow = thisLine.line_code;
+ createComponent({}, store);
+
+ expect(findNewTd().classes()).toContain('hll');
+ expect(findOldTd().classes()).toContain('hll');
+ });
+
+ it('does not highlight when isHighlighted false', () => {
+ createComponent();
+
+ expect(findNewTd().classes()).not.toContain('hll');
+ expect(findOldTd().classes()).not.toContain('hll');
+ });
+ });
+
+ describe('comment button', () => {
+ const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
+
+ it.each`
+ userData | query | mergeRefHeadComments | expectation
+ ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true}
+ ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true}
+ ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false}
+ ${null} | ${''} | ${true} | ${false}
+ `(
+ 'exists is $expectation - with userData ($userData) query ($query)',
+ ({ userData, query, mergeRefHeadComments, expectation }) => {
+ store.state.notes.userData = userData;
+ gon.features = { mergeRefHeadComments };
+ setWindowLocation({ href: `${TEST_HOST}?${query}` });
+ createComponent({}, store);
+
+ expect(findNoteButton().exists()).toBe(expectation);
+ },
+ );
+
+ it.each`
+ isHover | line | expectation
+ ${true} | ${{ ...thisLine, discussions: [] }} | ${true}
+ ${false} | ${{ ...thisLine, discussions: [] }} | ${false}
+ ${true} | ${{ ...thisLine, type: 'context', discussions: [] }} | ${false}
+ ${true} | ${{ ...thisLine, type: 'old-nonewline', discussions: [] }} | ${false}
+ ${true} | ${{ ...thisLine, discussions: [{}] }} | ${false}
+ `('visible is $expectation - line ($line)', ({ isHover, line, expectation }) => {
+ createComponent({ line });
+ wrapper.setData({ isHover });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findNoteButton().isVisible()).toBe(expectation);
+ });
+ });
+
+ it.each`
+ disabled | commentsDisabled
+ ${'disabled'} | ${true}
+ ${undefined} | ${false}
+ `(
+ 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
+ ({ disabled, commentsDisabled }) => {
+ createComponent({
+ line: { ...thisLine, commentsDisabled },
+ });
+
+ wrapper.setData({ isHover: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findNoteButton().attributes('disabled')).toBe(disabled);
+ });
+ },
+ );
+
+ const symlinkishFileTooltip =
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
+ const realishFileTooltip =
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
+ const otherFileTooltip = 'Add a comment to this line';
+ const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' });
+
+ it.each`
+ tooltip | commentsDisabled
+ ${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
+ ${symlinkishFileTooltip} | ${{ isSymbolic: true }}
+ ${realishFileTooltip} | ${{ wasReal: true }}
+ ${realishFileTooltip} | ${{ isReal: true }}
+ ${otherFileTooltip} | ${false}
+ `(
+ 'has the correct tooltip when commentsDisabled=$commentsDisabled',
+ ({ tooltip, commentsDisabled }) => {
+ createComponent({
+ line: { ...thisLine, commentsDisabled },
+ });
+
+ wrapper.setData({ isHover: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findTooltip().attributes('title')).toBe(tooltip);
+ });
+ },
+ );
});
- it('for lines without coverage', done => {
- vm.$nextTick()
- .then(() => {
- const name = diffFileMockData.file_path;
- const line = thisLine.new_line;
+ describe('line number', () => {
+ const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' });
+ const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' });
+
+ it('renders line numbers in correct cells', () => {
+ createComponent();
+
+ expect(findLineNumberOld().exists()).toBe(false);
+ expect(findLineNumberNew().exists()).toBe(true);
+ });
- vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } };
+ describe('with lineNumber prop', () => {
+ const TEST_LINE_CODE = 'LC_42';
+ const TEST_LINE_NUMBER = 1;
- return vm.$nextTick();
- })
- .then(() => {
- const coverage = wrapper.find('.line-coverage');
+ describe.each`
+ lineProps | findLineNumber | expectedHref | expectedClickArg
+ ${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE}
+ ${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined}
+ ${{ line_code: undefined, left: { line_code: TEST_LINE_CODE }, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${TEST_LINE_CODE}
+ ${{ line_code: undefined, right: { line_code: TEST_LINE_CODE }, new_line: TEST_LINE_NUMBER }} | ${findLineNumberNew} | ${'#'} | ${TEST_LINE_CODE}
+ `(
+ 'with line ($lineProps)',
+ ({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ createComponent({
+ line: { ...thisLine, ...lineProps },
+ });
+ });
- expect(coverage.attributes('title')).toContain('No test coverage');
- expect(coverage.classes('no-coverage')).toBe(true);
- })
- .then(done)
- .catch(done.fail);
+ it('renders', () => {
+ expect(findLineNumber().exists()).toBe(true);
+ expect(findLineNumber().attributes()).toEqual({
+ href: expectedHref,
+ 'data-linenumber': TEST_LINE_NUMBER.toString(),
+ });
+ });
+
+ it('on click, dispatches setHighlightedRow', () => {
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+
+ findLineNumber().trigger('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'diffs/setHighlightedRow',
+ expectedClickArg,
+ );
+ expect(store.dispatch).toHaveBeenCalledTimes(2);
+ });
+ },
+ );
+ });
});
- it('for unknown lines', done => {
- vm.$nextTick()
- .then(() => {
- vm.$store.state.diffs.coverageFiles = {};
-
- return vm.$nextTick();
- })
- .then(() => {
- const coverage = wrapper.find('.line-coverage');
-
- expect(coverage.attributes('title')).toBeUndefined();
- expect(coverage.classes('coverage')).toBe(false);
- expect(coverage.classes('no-coverage')).toBe(false);
- })
- .then(done)
- .catch(done.fail);
+ describe('diff-gutter-avatars', () => {
+ const TEST_LINE_CODE = 'LC_42';
+ const TEST_FILE_HASH = diffFileMockData.file_hash;
+ const findAvatars = () => wrapper.find(DiffGutterAvatars);
+ let line;
+
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ line = {
+ line_code: TEST_LINE_CODE,
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ discussions: [{ ...discussionsMockData }],
+ discussionsExpanded: true,
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ };
+ });
+
+ describe('with showCommentButton', () => {
+ it('renders if line has discussions', () => {
+ createComponent({ line });
+
+ expect(findAvatars().props()).toEqual({
+ discussions: line.discussions,
+ discussionsExpanded: line.discussionsExpanded,
+ });
+ });
+
+ it('does notrender if line has no discussions', () => {
+ line.discussions = [];
+ createComponent({ line });
+
+ expect(findAvatars().exists()).toEqual(false);
+ });
+
+ it('toggles line discussion', () => {
+ createComponent({ line });
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+
+ findAvatars().vm.$emit('toggleLineDiscussions');
+
+ expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', {
+ lineCode: TEST_LINE_CODE,
+ fileHash: TEST_FILE_HASH,
+ expanded: !line.discussionsExpanded,
+ });
+ });
+ });
});
});
});
diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js
index 6c37f86658e..39c581e2796 100644
--- a/spec/frontend/diffs/components/inline_diff_view_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_view_spec.js
@@ -30,8 +30,8 @@ describe('InlineDiffView', () => {
it('should have rendered diff lines', () => {
const el = component.$el;
- expect(el.querySelectorAll('tr.line_holder').length).toEqual(5);
- expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2);
+ expect(el.querySelectorAll('tr.line_holder').length).toEqual(8);
+ expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(4);
expect(el.querySelectorAll('tr.line_expansion.match').length).toEqual(1);
expect(el.textContent.indexOf('Bad dates')).toBeGreaterThan(-1);
});
diff --git a/spec/frontend/diffs/components/merge_conflict_warning_spec.js b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
new file mode 100644
index 00000000000..2f303f25f66
--- /dev/null
+++ b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import MergeConflictWarning from '~/diffs/components/merge_conflict_warning.vue';
+import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
+
+const propsData = {
+ limited: true,
+ mergeable: true,
+ resolutionPath: 'a-path',
+};
+const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
+
+function findResolveButton(wrapper) {
+ return wrapper.find('.gl-alert-actions a.gl-button:first-child');
+}
+function findLocalMergeButton(wrapper) {
+ return wrapper.find('.gl-alert-actions button.gl-button:last-child');
+}
+
+describe('MergeConflictWarning', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, { full } = { full: false }) => {
+ const mounter = full ? mount : shallowMount;
+
+ wrapper = mounter(MergeConflictWarning, {
+ propsData: { ...propsData, ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ limited | containerClasses
+ ${true} | ${limitedClasses}
+ ${false} | ${[]}
+ `(
+ 'has the correct container classes when limited is $limited',
+ ({ limited, containerClasses }) => {
+ createComponent({ limited });
+
+ expect(wrapper.classes()).toEqual(containerClasses);
+ },
+ );
+
+ it.each`
+ present | resolutionPath
+ ${false} | ${''}
+ ${true} | ${'some-path'}
+ `(
+ 'toggles the resolve conflicts button based on the provided resolutionPath "$resolutionPath"',
+ ({ present, resolutionPath }) => {
+ createComponent({ resolutionPath }, { full: true });
+ const resolveButton = findResolveButton(wrapper);
+
+ expect(resolveButton.exists()).toBe(present);
+ if (present) {
+ expect(resolveButton.attributes('href')).toBe(resolutionPath);
+ }
+ },
+ );
+
+ it.each`
+ present | mergeable
+ ${false} | ${false}
+ ${true} | ${true}
+ `(
+ 'toggles the local merge button based on the provided mergeable property "$mergable"',
+ ({ present, mergeable }) => {
+ createComponent({ mergeable }, { full: true });
+ const localMerge = findLocalMergeButton(wrapper);
+
+ expect(localMerge.exists()).toBe(present);
+ },
+ );
+});
diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index 2795c68b4ee..78805a1cddc 100644
--- a/spec/frontend/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
@@ -36,7 +36,7 @@ describe('Diff no changes empty state', () => {
};
});
- expect(vm.contains('script')).toBe(false);
+ expect(vm.find('script').exists()).toBe(false);
});
describe('Renders', () => {
diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
index 339352943a9..13c4ce06f18 100644
--- a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import { createStore } from '~/mr_notes/stores';
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import diffFileMockData from '../mock_data/diff_file';
+import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
+import discussionsMockData from '../mock_data/diff_discussions';
describe('ParallelDiffTableRow', () => {
describe('when one side is empty', () => {
@@ -158,4 +161,260 @@ describe('ParallelDiffTableRow', () => {
});
});
});
+
+ describe('Table Cells', () => {
+ let wrapper;
+ let store;
+ let thisLine;
+ const TEST_USER_ID = 'abc123';
+ const TEST_USER = { id: TEST_USER_ID };
+
+ const createComponent = (props = {}, propsStore = store, data = {}) => {
+ wrapper = shallowMount(ParallelDiffTableRow, {
+ store: propsStore,
+ propsData: {
+ line: thisLine,
+ fileHash: diffFileMockData.file_hash,
+ filePath: diffFileMockData.file_path,
+ contextLinesPath: 'contextLinesPath',
+ isHighlighted: false,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ });
+ };
+
+ const setWindowLocation = value => {
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value,
+ });
+ };
+
+ beforeEach(() => {
+ // eslint-disable-next-line prefer-destructuring
+ thisLine = diffFileMockData.parallel_diff_lines[2];
+ store = createStore();
+ store.state.notes.userData = TEST_USER;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findNewTd = () => wrapper.find({ ref: 'newTd' });
+ const findOldTd = () => wrapper.find({ ref: 'oldTd' });
+
+ describe('td', () => {
+ it('highlights when isHighlighted true', () => {
+ store.state.diffs.highlightedRow = thisLine.left.line_code;
+ createComponent({}, store);
+
+ expect(findNewTd().classes()).toContain('hll');
+ expect(findOldTd().classes()).toContain('hll');
+ });
+
+ it('does not highlight when isHighlighted false', () => {
+ createComponent();
+
+ expect(findNewTd().classes()).not.toContain('hll');
+ expect(findOldTd().classes()).not.toContain('hll');
+ });
+ });
+
+ describe('comment button', () => {
+ const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButtonLeft' });
+
+ it.each`
+ hover | line | userData | query | mergeRefHeadComments | expectation
+ ${true} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true}
+ ${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false}
+ ${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true}
+ ${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false}
+ ${true} | ${{}} | ${null} | ${''} | ${true} | ${false}
+ ${false} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false}
+ `(
+ 'exists is $expectation - with userData ($userData) query ($query)',
+ async ({ hover, line, userData, query, mergeRefHeadComments, expectation }) => {
+ store.state.notes.userData = userData;
+ gon.features = { mergeRefHeadComments };
+ setWindowLocation({ href: `${TEST_HOST}?${query}` });
+ createComponent(line, store);
+ if (hover) await wrapper.find('.line_holder').trigger('mouseover');
+
+ expect(findNoteButton().exists()).toBe(expectation);
+ },
+ );
+
+ it.each`
+ line | expectation
+ ${{ ...thisLine, left: { discussions: [] } }} | ${true}
+ ${{ ...thisLine, left: { type: 'context', discussions: [] } }} | ${false}
+ ${{ ...thisLine, left: { type: 'old-nonewline', discussions: [] } }} | ${false}
+ ${{ ...thisLine, left: { discussions: [{}] } }} | ${false}
+ `('visible is $expectation - line ($line)', async ({ line, expectation }) => {
+ createComponent({ line }, store, { isLeftHover: true, isCommentButtonRendered: true });
+
+ expect(findNoteButton().isVisible()).toBe(expectation);
+ });
+
+ it.each`
+ disabled | commentsDisabled
+ ${'disabled'} | ${true}
+ ${undefined} | ${false}
+ `(
+ 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
+ ({ disabled, commentsDisabled }) => {
+ thisLine.left.commentsDisabled = commentsDisabled;
+ createComponent({ line: { ...thisLine } }, store, {
+ isLeftHover: true,
+ isCommentButtonRendered: true,
+ });
+
+ expect(findNoteButton().attributes('disabled')).toBe(disabled);
+ },
+ );
+
+ const symlinkishFileTooltip =
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
+ const realishFileTooltip =
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
+ const otherFileTooltip = 'Add a comment to this line';
+ const findTooltip = () => wrapper.find({ ref: 'addNoteTooltipLeft' });
+
+ it.each`
+ tooltip | commentsDisabled
+ ${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
+ ${symlinkishFileTooltip} | ${{ isSymbolic: true }}
+ ${realishFileTooltip} | ${{ wasReal: true }}
+ ${realishFileTooltip} | ${{ isReal: true }}
+ ${otherFileTooltip} | ${false}
+ `(
+ 'has the correct tooltip when commentsDisabled=$commentsDisabled',
+ ({ tooltip, commentsDisabled }) => {
+ thisLine.left.commentsDisabled = commentsDisabled;
+ createComponent({ line: { ...thisLine } }, store, {
+ isLeftHover: true,
+ isCommentButtonRendered: true,
+ });
+
+ expect(findTooltip().attributes('title')).toBe(tooltip);
+ },
+ );
+ });
+
+ describe('line number', () => {
+ const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' });
+ const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' });
+
+ it('renders line numbers in correct cells', () => {
+ createComponent();
+
+ expect(findLineNumberOld().exists()).toBe(true);
+ expect(findLineNumberNew().exists()).toBe(true);
+ });
+
+ describe('with lineNumber prop', () => {
+ const TEST_LINE_CODE = 'LC_42';
+ const TEST_LINE_NUMBER = 1;
+
+ describe.each`
+ lineProps | findLineNumber | expectedHref | expectedClickArg
+ ${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE}
+ ${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined}
+ `(
+ 'with line ($lineProps)',
+ ({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ Object.assign(thisLine.left, lineProps);
+ Object.assign(thisLine.right, lineProps);
+ createComponent({
+ line: { ...thisLine },
+ });
+ });
+
+ it('renders', () => {
+ expect(findLineNumber().exists()).toBe(true);
+ expect(findLineNumber().attributes()).toEqual({
+ href: expectedHref,
+ 'data-linenumber': TEST_LINE_NUMBER.toString(),
+ });
+ });
+
+ it('on click, dispatches setHighlightedRow', () => {
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+
+ findLineNumber().trigger('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'diffs/setHighlightedRow',
+ expectedClickArg,
+ );
+ expect(store.dispatch).toHaveBeenCalledTimes(2);
+ });
+ },
+ );
+ });
+ });
+
+ describe('diff-gutter-avatars', () => {
+ const TEST_LINE_CODE = 'LC_42';
+ const TEST_FILE_HASH = diffFileMockData.file_hash;
+ const findAvatars = () => wrapper.find(DiffGutterAvatars);
+ let line;
+
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ line = {
+ left: {
+ line_code: TEST_LINE_CODE,
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ discussions: [{ ...discussionsMockData }],
+ discussionsExpanded: true,
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ };
+ });
+
+ describe('with showCommentButton', () => {
+ it('renders if line has discussions', () => {
+ createComponent({ line });
+
+ expect(findAvatars().props()).toEqual({
+ discussions: line.left.discussions,
+ discussionsExpanded: line.left.discussionsExpanded,
+ });
+ });
+
+ it('does notrender if line has no discussions', () => {
+ line.left.discussions = [];
+ createComponent({ line });
+
+ expect(findAvatars().exists()).toEqual(false);
+ });
+
+ it('toggles line discussion', () => {
+ createComponent({ line });
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+
+ findAvatars().vm.$emit('toggleLineDiscussions');
+
+ expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', {
+ lineCode: TEST_LINE_CODE,
+ fileHash: TEST_FILE_HASH,
+ expanded: !line.left.discussionsExpanded,
+ });
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js
index cb1a47f60d5..44ed303d0ef 100644
--- a/spec/frontend/diffs/components/parallel_diff_view_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_view_spec.js
@@ -1,33 +1,37 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { createStore } from '~/mr_notes/stores';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
-import * as constants from '~/diffs/constants';
+import parallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import diffFileMockData from '../mock_data/diff_file';
-describe('ParallelDiffView', () => {
- let component;
- const getDiffFileMock = () => ({ ...diffFileMockData });
+let wrapper;
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
- beforeEach(() => {
- const diffFile = getDiffFileMock();
+function factory() {
+ const diffFile = { ...diffFileMockData };
+ const store = createStore();
- component = createComponentWithStore(Vue.extend(ParallelDiffView), createStore(), {
+ wrapper = shallowMount(ParallelDiffView, {
+ localVue,
+ store,
+ propsData: {
diffFile,
diffLines: diffFile.parallel_diff_lines,
- }).$mount();
+ },
});
+}
+describe('ParallelDiffView', () => {
afterEach(() => {
- component.$destroy();
+ wrapper.destroy();
});
- describe('assigned', () => {
- describe('diffLines', () => {
- it('should normalize lines for empty cells', () => {
- expect(component.diffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE);
- expect(component.diffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE);
- });
- });
+ it('renders diff lines', () => {
+ factory();
+
+ expect(wrapper.findAll(parallelDiffTableRow).length).toBe(8);
});
});
diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js
index 2e95d79ea49..72330d8efba 100644
--- a/spec/frontend/diffs/components/settings_dropdown_spec.js
+++ b/spec/frontend/diffs/components/settings_dropdown_spec.js
@@ -7,7 +7,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constant
const localVue = createLocalVue();
localVue.use(Vuex);
-describe('Diff settiings dropdown component', () => {
+describe('Diff settings dropdown component', () => {
let vm;
let actions;
@@ -50,7 +50,7 @@ describe('Diff settiings dropdown component', () => {
vm.find('.js-list-view').trigger('click');
- expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), false, undefined);
+ expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), false);
});
it('tree view button dispatches setRenderTreeList with true', () => {
@@ -58,53 +58,53 @@ describe('Diff settiings dropdown component', () => {
vm.find('.js-tree-view').trigger('click');
- expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true, undefined);
+ expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true);
});
- it('sets list button as active when renderTreeList is false', () => {
+ it('sets list button as selected when renderTreeList is false', () => {
createComponent(store => {
Object.assign(store.state.diffs, {
renderTreeList: false,
});
});
- expect(vm.find('.js-list-view').classes('active')).toBe(true);
- expect(vm.find('.js-tree-view').classes('active')).toBe(false);
+ expect(vm.find('.js-list-view').classes('selected')).toBe(true);
+ expect(vm.find('.js-tree-view').classes('selected')).toBe(false);
});
- it('sets tree button as active when renderTreeList is true', () => {
+ it('sets tree button as selected when renderTreeList is true', () => {
createComponent(store => {
Object.assign(store.state.diffs, {
renderTreeList: true,
});
});
- expect(vm.find('.js-list-view').classes('active')).toBe(false);
- expect(vm.find('.js-tree-view').classes('active')).toBe(true);
+ expect(vm.find('.js-list-view').classes('selected')).toBe(false);
+ expect(vm.find('.js-tree-view').classes('selected')).toBe(true);
});
});
describe('compare changes', () => {
- it('sets inline button as active', () => {
+ it('sets inline button as selected', () => {
createComponent(store => {
Object.assign(store.state.diffs, {
diffViewType: INLINE_DIFF_VIEW_TYPE,
});
});
- expect(vm.find('.js-inline-diff-button').classes('active')).toBe(true);
- expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(false);
+ expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(true);
+ expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(false);
});
- it('sets parallel button as active', () => {
+ it('sets parallel button as selected', () => {
createComponent(store => {
Object.assign(store.state.diffs, {
diffViewType: PARALLEL_DIFF_VIEW_TYPE,
});
});
- expect(vm.find('.js-inline-diff-button').classes('active')).toBe(false);
- expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(true);
+ expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(false);
+ expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(true);
});
it('calls setInlineDiffViewType when clicking inline button', () => {
@@ -153,14 +153,10 @@ describe('Diff settiings dropdown component', () => {
checkbox.element.checked = true;
checkbox.trigger('change');
- expect(actions.setShowWhitespace).toHaveBeenCalledWith(
- expect.anything(),
- {
- showWhitespace: true,
- pushState: true,
- },
- undefined,
- );
+ expect(actions.setShowWhitespace).toHaveBeenCalledWith(expect.anything(), {
+ showWhitespace: true,
+ pushState: true,
+ });
});
});
});
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 14cb2a17aec..cc177a81d88 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -1,16 +1,26 @@
import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
+import FileTree from '~/vue_shared/components/file_tree.vue';
describe('Diffs tree list component', () => {
let wrapper;
+ let store;
const getFileRows = () => wrapper.findAll('.file-row');
const localVue = createLocalVue();
localVue.use(Vuex);
- const createComponent = state => {
- const store = new Vuex.Store({
+ const createComponent = (mountFn = mount) => {
+ wrapper = mountFn(TreeList, {
+ store,
+ localVue,
+ propsData: { hideFileStats: false },
+ });
+ };
+
+ beforeEach(() => {
+ store = new Vuex.Store({
modules: {
diffs: createStore(),
},
@@ -23,61 +33,57 @@ describe('Diffs tree list component', () => {
addedLines: 10,
removedLines: 20,
...store.state.diffs,
- ...state,
};
+ });
- wrapper = mount(TreeList, {
- store,
- localVue,
- propsData: { hideFileStats: false },
+ const setupFilesInState = () => {
+ const treeEntries = {
+ 'index.js': {
+ addedLines: 0,
+ changed: true,
+ deleted: false,
+ fileHash: 'test',
+ key: 'index.js',
+ name: 'index.js',
+ path: 'app/index.js',
+ removedLines: 0,
+ tempFile: true,
+ type: 'blob',
+ parentPath: 'app',
+ },
+ app: {
+ key: 'app',
+ path: 'app',
+ name: 'app',
+ type: 'tree',
+ tree: [],
+ },
+ };
+
+ Object.assign(store.state.diffs, {
+ treeEntries,
+ tree: [treeEntries['index.js'], treeEntries.app],
});
};
- beforeEach(() => {
- localStorage.removeItem('mr_diff_tree_list');
-
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders empty text', () => {
- expect(wrapper.text()).toContain('No files found');
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders empty text', () => {
+ expect(wrapper.text()).toContain('No files found');
+ });
});
describe('with files', () => {
beforeEach(() => {
- const treeEntries = {
- 'index.js': {
- addedLines: 0,
- changed: true,
- deleted: false,
- fileHash: 'test',
- key: 'index.js',
- name: 'index.js',
- path: 'app/index.js',
- removedLines: 0,
- tempFile: true,
- type: 'blob',
- parentPath: 'app',
- },
- app: {
- key: 'app',
- path: 'app',
- name: 'app',
- type: 'tree',
- tree: [],
- },
- };
-
- createComponent({
- treeEntries,
- tree: [treeEntries['index.js'], treeEntries.app],
- });
-
- return wrapper.vm.$nextTick();
+ setupFilesInState();
+ createComponent();
});
it('renders tree', () => {
@@ -136,4 +142,23 @@ describe('Diffs tree list component', () => {
});
});
});
+
+ describe('with viewedDiffFileIds', () => {
+ const viewedDiffFileIds = { fileId: '#12345' };
+
+ beforeEach(() => {
+ setupFilesInState();
+ store.state.diffs.viewedDiffFileIds = viewedDiffFileIds;
+ });
+
+ it('passes the viewedDiffFileIds to the FileTree', () => {
+ createComponent(shallowMount);
+
+ return wrapper.vm.$nextTick().then(() => {
+ // Have to use $attrs['viewed-files'] because we are passing down an object
+ // and attributes('') stringifies values (e.g. [object])...
+ expect(wrapper.find(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
+ });
+ });
+ });
});
diff --git a/spec/frontend/diffs/mock_data/diff_file.js b/spec/frontend/diffs/mock_data/diff_file.js
index e4b2fdf6ede..c2a4424ee95 100644
--- a/spec/frontend/diffs/mock_data/diff_file.js
+++ b/spec/frontend/diffs/mock_data/diff_file.js
@@ -56,8 +56,8 @@ export default {
old_line: null,
new_line: 1,
discussions: [],
- text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
- rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
},
{
@@ -66,8 +66,8 @@ export default {
old_line: null,
new_line: 2,
discussions: [],
- text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
- rich_text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
meta_data: null,
},
{
@@ -76,8 +76,8 @@ export default {
old_line: 1,
new_line: 3,
discussions: [],
- text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
- rich_text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
},
{
@@ -86,8 +86,8 @@ export default {
old_line: 2,
new_line: 4,
discussions: [],
- text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
- rich_text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
},
{
@@ -96,8 +96,38 @@ export default {
old_line: 3,
new_line: 5,
discussions: [],
- text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
- rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_6',
+ type: 'old',
+ old_line: 4,
+ new_line: null,
+ discussions: [],
+ text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_7',
+ type: 'new',
+ old_line: null,
+ new_line: 5,
+ discussions: [],
+ text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_9',
+ type: 'new',
+ old_line: null,
+ new_line: 6,
+ discussions: [],
+ text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
},
{
@@ -116,43 +146,39 @@ export default {
],
parallel_diff_lines: [
{
- left: {
- type: 'empty-cell',
- },
+ left: null,
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
type: 'new',
old_line: null,
new_line: 1,
discussions: [],
- text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
},
},
{
- left: {
- type: 'empty-cell',
- },
+ left: null,
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
type: 'new',
old_line: null,
new_line: 2,
discussions: [],
- text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
meta_data: null,
},
},
{
left: {
- line_Code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
type: null,
old_line: 1,
new_line: 3,
discussions: [],
- text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
},
@@ -162,7 +188,7 @@ export default {
old_line: 1,
new_line: 3,
discussions: [],
- text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
},
@@ -174,7 +200,7 @@ export default {
old_line: 2,
new_line: 4,
discussions: [],
- text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
},
@@ -184,7 +210,7 @@ export default {
old_line: 2,
new_line: 4,
discussions: [],
- text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
},
@@ -196,7 +222,7 @@ export default {
old_line: 3,
new_line: 5,
discussions: [],
- text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
},
@@ -206,13 +232,48 @@ export default {
old_line: 3,
new_line: 5,
discussions: [],
- text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
},
},
{
left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_6',
+ type: 'old',
+ old_line: 4,
+ new_line: null,
+ discussions: [],
+ text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_7',
+ type: 'new',
+ old_line: null,
+ new_line: 5,
+ discussions: [],
+ text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: null,
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_9',
+ type: 'new',
+ old_line: null,
+ new_line: 6,
+ discussions: [],
+ text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
line_code: null,
type: 'match',
old_line: null,
diff --git a/spec/frontend/diffs/mock_data/diff_metadata.js b/spec/frontend/diffs/mock_data/diff_metadata.js
index b73b29e4bc8..cfa0038c06f 100644
--- a/spec/frontend/diffs/mock_data/diff_metadata.js
+++ b/spec/frontend/diffs/mock_data/diff_metadata.js
@@ -1,6 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-/* https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20 */
-
export const diffMetadata = {
real_size: '1',
size: 1,
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 5fef35d6c5b..4f647b0cd41 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -13,7 +13,6 @@ import {
} from '~/diffs/constants';
import {
setBaseConfig,
- fetchDiffFiles,
fetchDiffFilesBatch,
fetchDiffFilesMeta,
fetchCoverageFiles,
@@ -101,7 +100,6 @@ describe('DiffsStoreActions', () => {
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
const showSuggestPopover = false;
- const useSingleDiffStyle = false;
testAction(
setBaseConfig,
@@ -113,7 +111,6 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
},
{
endpoint: '',
@@ -123,7 +120,6 @@ describe('DiffsStoreActions', () => {
projectPath: '',
dismissEndpoint: '',
showSuggestPopover: true,
- useSingleDiffStyle: true,
},
[
{
@@ -136,7 +132,6 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
- useSingleDiffStyle,
},
},
],
@@ -146,39 +141,6 @@ describe('DiffsStoreActions', () => {
});
});
- describe('fetchDiffFiles', () => {
- it('should fetch diff files', done => {
- const endpoint = '/fetch/diff/files?view=inline&w=1';
- const mock = new MockAdapter(axios);
- const res = { diff_files: 1, merge_request_diffs: [] };
- mock.onGet(endpoint).reply(200, res);
-
- testAction(
- fetchDiffFiles,
- {},
- { endpoint, diffFiles: [], showWhitespace: false, diffViewType: 'inline' },
- [
- { type: types.SET_LOADING, payload: true },
- { type: types.SET_LOADING, payload: false },
- { type: types.SET_MERGE_REQUEST_DIFFS, payload: res.merge_request_diffs },
- { type: types.SET_DIFF_DATA, payload: res },
- ],
- [],
- () => {
- mock.restore();
- done();
- },
- );
-
- fetchDiffFiles({ state: { endpoint }, commit: () => null })
- .then(data => {
- expect(data).toEqual(res);
- done();
- })
- .catch(done.fail);
- });
- });
-
describe('fetchDiffFilesBatch', () => {
let mock;
@@ -223,16 +185,16 @@ describe('DiffsStoreActions', () => {
testAction(
fetchDiffFilesBatch,
{},
- { endpointBatch, useSingleDiffStyle: true, diffViewType: 'inline' },
+ { endpointBatch, diffViewType: 'inline' },
[
{ type: types.SET_BATCH_LOADING, payload: true },
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false },
- { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test' },
+ { type: types.VIEW_DIFF_FILE, payload: 'test' },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false },
- { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test2' },
+ { type: types.VIEW_DIFF_FILE, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
],
[],
@@ -253,7 +215,6 @@ describe('DiffsStoreActions', () => {
commit: () => {},
state: {
endpointBatch: `${endpointBatch}?view=${otherView}`,
- useSingleDiffStyle: true,
diffViewType: viewStyle,
},
})
@@ -283,7 +244,7 @@ describe('DiffsStoreActions', () => {
testAction(
fetchDiffFilesMeta,
{},
- { endpointMetadata },
+ { endpointMetadata, diffViewType: 'inline' },
[
{ type: types.SET_LOADING, payload: true },
{ type: types.SET_LOADING, payload: false },
@@ -299,146 +260,6 @@ describe('DiffsStoreActions', () => {
});
});
- describe('when the single diff view feature flag is off', () => {
- describe('fetchDiffFiles', () => {
- it('should fetch diff files', done => {
- const endpoint = '/fetch/diff/files?w=1';
- const mock = new MockAdapter(axios);
- const res = { diff_files: 1, merge_request_diffs: [] };
- mock.onGet(endpoint).reply(200, res);
-
- testAction(
- fetchDiffFiles,
- {},
- {
- endpoint,
- diffFiles: [],
- showWhitespace: false,
- diffViewType: 'inline',
- useSingleDiffStyle: false,
- currentDiffFileId: null,
- },
- [
- { type: types.SET_LOADING, payload: true },
- { type: types.SET_LOADING, payload: false },
- { type: types.SET_MERGE_REQUEST_DIFFS, payload: res.merge_request_diffs },
- { type: types.SET_DIFF_DATA, payload: res },
- ],
- [],
- () => {
- mock.restore();
- done();
- },
- );
-
- fetchDiffFiles({ state: { endpoint }, commit: () => null })
- .then(data => {
- expect(data).toEqual(res);
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('fetchDiffFilesBatch', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should fetch batch diff files', done => {
- const endpointBatch = '/fetch/diffs_batch';
- const res1 = { diff_files: [{ file_hash: 'test' }], pagination: { next_page: 2 } };
- const res2 = { diff_files: [{ file_hash: 'test2' }], pagination: {} };
- mock
- .onGet(mergeUrlParams({ per_page: DIFFS_PER_PAGE, w: '1', page: 1 }, endpointBatch))
- .reply(200, res1)
- .onGet(mergeUrlParams({ per_page: DIFFS_PER_PAGE, w: '1', page: 2 }, endpointBatch))
- .reply(200, res2);
-
- testAction(
- fetchDiffFilesBatch,
- {},
- { endpointBatch, useSingleDiffStyle: false, currentDiffFileId: null },
- [
- { type: types.SET_BATCH_LOADING, payload: true },
- { type: types.SET_RETRIEVING_BATCHES, payload: true },
- { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
- { type: types.SET_BATCH_LOADING, payload: false },
- { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test' },
- { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
- { type: types.SET_BATCH_LOADING, payload: false },
- { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'test2' },
- { type: types.SET_RETRIEVING_BATCHES, payload: false },
- ],
- [],
- done,
- );
- });
-
- it.each`
- querystrings | requestUrl
- ${'?view=parallel'} | ${'/fetch/diffs_batch?view=parallel'}
- ${'?view=inline'} | ${'/fetch/diffs_batch?view=inline'}
- ${''} | ${'/fetch/diffs_batch'}
- `(
- 'should use the endpoint $requestUrl if the endpointBatch in state includes `$querystrings` as a querystring',
- ({ querystrings, requestUrl }) => {
- const endpointBatch = '/fetch/diffs_batch';
-
- fetchDiffFilesBatch({
- commit: () => {},
- state: {
- endpointBatch: `${endpointBatch}${querystrings}`,
- diffViewType: 'inline',
- },
- })
- .then(() => {
- expect(mock.history.get[0].url).toEqual(requestUrl);
- })
- .catch(() => {});
- },
- );
- });
-
- describe('fetchDiffFilesMeta', () => {
- const endpointMetadata = '/fetch/diffs_metadata.json';
- const noFilesData = { ...diffMetadata };
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- delete noFilesData.diff_files;
-
- mock.onGet(endpointMetadata).reply(200, diffMetadata);
- });
- it('should fetch diff meta information', done => {
- testAction(
- fetchDiffFilesMeta,
- {},
- { endpointMetadata, useSingleDiffStyle: false },
- [
- { type: types.SET_LOADING, payload: true },
- { type: types.SET_LOADING, payload: false },
- { type: types.SET_MERGE_REQUEST_DIFFS, payload: diffMetadata.merge_request_diffs },
- { type: types.SET_DIFF_DATA, payload: noFilesData },
- ],
- [],
- () => {
- mock.restore();
- done();
- },
- );
- });
- });
- });
-
describe('fetchCoverageFiles', () => {
let mock;
const endpointCoverage = '/fetch';
@@ -479,7 +300,7 @@ describe('DiffsStoreActions', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
testAction(setHighlightedRow, 'ABC_123', {}, [
{ type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
- { type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'ABC' },
+ { type: types.VIEW_DIFF_FILE, payload: 'ABC' },
]);
});
});
@@ -589,7 +410,7 @@ describe('DiffsStoreActions', () => {
testAction(
assignDiscussionsToDiff,
[],
- { diffFiles: [], useSingleDiffStyle: true },
+ { diffFiles: [] },
[],
[{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
done,
@@ -1083,7 +904,7 @@ describe('DiffsStoreActions', () => {
expect(document.location.hash).toBe('#test');
});
- it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
+ it('commits VIEW_DIFF_FILE', () => {
const state = {
treeEntries: {
path: {
@@ -1094,7 +915,7 @@ describe('DiffsStoreActions', () => {
scrollToFile({ state, commit }, 'path');
- expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, 'test');
+ expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, 'test');
});
});
@@ -1592,7 +1413,7 @@ describe('DiffsStoreActions', () => {
});
describe('setCurrentDiffFileIdFromNote', () => {
- it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
+ it('commits VIEW_DIFF_FILE', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1602,10 +1423,10 @@ describe('DiffsStoreActions', () => {
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
- expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, '123');
+ expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, '123');
});
- it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when discussion has no diff_file', () => {
+ it('does not commit VIEW_DIFF_FILE when discussion has no diff_file', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1618,7 +1439,7 @@ describe('DiffsStoreActions', () => {
expect(commit).not.toHaveBeenCalled();
});
- it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when diff file does not exist', () => {
+ it('does not commit VIEW_DIFF_FILE when diff file does not exist', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1633,12 +1454,12 @@ describe('DiffsStoreActions', () => {
});
describe('navigateToDiffFileIndex', () => {
- it('commits UPDATE_CURRENT_DIFF_FILE_ID', done => {
+ it('commits VIEW_DIFF_FILE', done => {
testAction(
navigateToDiffFileIndex,
0,
{ diffFiles: [{ file_hash: '123' }] },
- [{ type: types.UPDATE_CURRENT_DIFF_FILE_ID, payload: '123' }],
+ [{ type: types.VIEW_DIFF_FILE, payload: '123' }],
[],
done,
);
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index 70047899612..e1d855ae0cf 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -11,13 +11,11 @@ describe('DiffsStoreMutations', () => {
const state = {};
const endpoint = '/diffs/endpoint';
const projectPath = '/root/project';
- const useSingleDiffStyle = false;
- mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath, useSingleDiffStyle });
+ mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath });
expect(state.endpoint).toEqual(endpoint);
expect(state.projectPath).toEqual(projectPath);
- expect(state.useSingleDiffStyle).toEqual(useSingleDiffStyle);
});
});
@@ -70,12 +68,13 @@ describe('DiffsStoreMutations', () => {
});
describe('SET_DIFF_DATA', () => {
- it('should set diff data type properly', () => {
+ it('should not modify the existing state', () => {
const state = {
diffFiles: [
{
- ...diffFileMockData,
- parallel_diff_lines: [],
+ content_sha: diffFileMockData.content_sha,
+ file_hash: diffFileMockData.file_hash,
+ highlighted_diff_lines: [],
},
],
};
@@ -85,43 +84,7 @@ describe('DiffsStoreMutations', () => {
mutations[types.SET_DIFF_DATA](state, diffMock);
- const firstLine = state.diffFiles[0].parallel_diff_lines[0];
-
- expect(firstLine.right.text).toBeUndefined();
- expect(state.diffFiles.length).toEqual(1);
- expect(state.diffFiles[0].renderIt).toEqual(true);
- expect(state.diffFiles[0].collapsed).toEqual(false);
- });
-
- describe('given diffsBatchLoad feature flag is enabled', () => {
- beforeEach(() => {
- gon.features = { diffsBatchLoad: true };
- });
-
- afterEach(() => {
- delete gon.features;
- });
-
- it('should not modify the existing state', () => {
- const state = {
- diffFiles: [
- {
- content_sha: diffFileMockData.content_sha,
- file_hash: diffFileMockData.file_hash,
- highlighted_diff_lines: [],
- },
- ],
- };
- const diffMock = {
- diff_files: [diffFileMockData],
- };
-
- mutations[types.SET_DIFF_DATA](state, diffMock);
-
- // If the batch load is enabled, there shouldn't be any processing
- // done on the existing state object, so we shouldn't have this.
- expect(state.diffFiles[0].parallel_diff_lines).toBeUndefined();
- });
+ expect(state.diffFiles[0].parallel_diff_lines).toBeUndefined();
});
});
@@ -682,6 +645,36 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].highlighted_diff_lines[0].discussions).toHaveLength(1);
expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toBe(1);
});
+
+ it('should add discussion to file', () => {
+ const state = {
+ latestDiff: true,
+ diffFiles: [
+ {
+ file_hash: 'ABC',
+ discussions: [],
+ parallel_diff_lines: [],
+ highlighted_diff_lines: [],
+ },
+ ],
+ };
+ const discussion = {
+ id: 1,
+ line_code: 'ABC_1',
+ diff_discussion: true,
+ resolvable: true,
+ diff_file: {
+ file_hash: state.diffFiles[0].file_hash,
+ },
+ };
+
+ mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
+ discussion,
+ diffPositionByLineCode: null,
+ });
+
+ expect(state.diffFiles[0].discussions.length).toEqual(1);
+ });
});
describe('REMOVE_LINE_DISCUSSIONS', () => {
@@ -774,11 +767,11 @@ describe('DiffsStoreMutations', () => {
});
});
- describe('UPDATE_CURRENT_DIFF_FILE_ID', () => {
+ describe('VIEW_DIFF_FILE', () => {
it('updates currentDiffFileId', () => {
const state = createState();
- mutations[types.UPDATE_CURRENT_DIFF_FILE_ID](state, 'somefileid');
+ mutations[types.VIEW_DIFF_FILE](state, 'somefileid');
expect(state.currentDiffFileId).toBe('somefileid');
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 62c82468ea0..39a482c85ae 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -1167,4 +1167,59 @@ describe('DiffsStoreUtils', () => {
expect(utils.getDefaultWhitespace(undefined, '0')).toBe(true);
});
});
+
+ describe('isAdded', () => {
+ it.each`
+ type | expected
+ ${'new'} | ${true}
+ ${'new-nonewline'} | ${true}
+ ${'old'} | ${false}
+ `('returns $expected when type is $type', ({ type, expected }) => {
+ expect(utils.isAdded({ type })).toBe(expected);
+ });
+ });
+
+ describe('isRemoved', () => {
+ it.each`
+ type | expected
+ ${'old'} | ${true}
+ ${'old-nonewline'} | ${true}
+ ${'new'} | ${false}
+ `('returns $expected when type is $type', ({ type, expected }) => {
+ expect(utils.isRemoved({ type })).toBe(expected);
+ });
+ });
+
+ describe('isUnchanged', () => {
+ it.each`
+ type | expected
+ ${null} | ${true}
+ ${'new'} | ${false}
+ ${'old'} | ${false}
+ `('returns $expected when type is $type', ({ type, expected }) => {
+ expect(utils.isUnchanged({ type })).toBe(expected);
+ });
+ });
+
+ describe('isMeta', () => {
+ it.each`
+ type | expected
+ ${'match'} | ${true}
+ ${'new-nonewline'} | ${true}
+ ${'old-nonewline'} | ${true}
+ ${'new'} | ${false}
+ `('returns $expected when type is $type', ({ type, expected }) => {
+ expect(utils.isMeta({ type })).toBe(expected);
+ });
+ });
+
+ describe('parallelizeDiffLines', () => {
+ it('converts inline diff lines to parallel diff lines', () => {
+ const file = getDiffFileMock();
+
+ expect(utils.parallelizeDiffLines(file.highlighted_diff_lines)).toEqual(
+ file.parallel_diff_lines,
+ );
+ });
+ });
});
diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/editor_lite_spec.js
index e4edeab172b..e566d3a4b38 100644
--- a/spec/frontend/editor/editor_lite_spec.js
+++ b/spec/frontend/editor/editor_lite_spec.js
@@ -1,8 +1,7 @@
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
import Editor from '~/editor/editor_lite';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
-
-const URI_PREFIX = 'gitlab';
+import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from '~/editor/constants';
describe('Base editor', () => {
let editorEl;
@@ -27,9 +26,7 @@ describe('Base editor', () => {
it('initializes Editor with basic properties', () => {
expect(editor).toBeDefined();
- expect(editor.editorEl).toBe(null);
- expect(editor.blobContent).toEqual('');
- expect(editor.blobPath).toEqual('');
+ expect(editor.instances).toEqual([]);
});
it('removes `editor-loading` data attribute from the target DOM element', () => {
@@ -51,15 +48,14 @@ describe('Base editor', () => {
instanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({
setModel,
dispose,
+ onDidDispose: jest.fn(),
}));
});
- it('does nothing if no dom element is supplied', () => {
- editor.createInstance();
-
- expect(editor.editorEl).toBe(null);
- expect(editor.blobContent).toEqual('');
- expect(editor.blobPath).toEqual('');
+ it('throws an error if no dom element is supplied', () => {
+ expect(() => {
+ editor.createInstance();
+ }).toThrow(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
expect(modelSpy).not.toHaveBeenCalled();
expect(instanceSpy).not.toHaveBeenCalled();
@@ -89,15 +85,133 @@ describe('Base editor', () => {
createUri(blobGlobalId, blobPath),
);
});
+
+ it('initializes instance with passed properties', () => {
+ const instanceOptions = {
+ foo: 'bar',
+ };
+ editor.createInstance({
+ el: editorEl,
+ ...instanceOptions,
+ });
+ expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.objectContaining(instanceOptions));
+ });
+
+ it('disposes instance when the editor is disposed', () => {
+ editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId });
+
+ expect(dispose).not.toHaveBeenCalled();
+
+ editor.dispose();
+
+ expect(dispose).toHaveBeenCalled();
+ });
+ });
+
+ describe('multiple instances', () => {
+ let instanceSpy;
+ let inst1Args;
+ let inst2Args;
+ let editorEl1;
+ let editorEl2;
+ let inst1;
+ let inst2;
+ const readOnlyIndex = '68'; // readOnly option has the internal index of 68 in the editor's options
+
+ beforeEach(() => {
+ setFixtures('<div id="editor1"></div><div id="editor2"></div>');
+ editorEl1 = document.getElementById('editor1');
+ editorEl2 = document.getElementById('editor2');
+ inst1Args = {
+ el: editorEl1,
+ blobGlobalId,
+ };
+ inst2Args = {
+ el: editorEl2,
+ blobContent,
+ blobPath,
+ blobGlobalId,
+ };
+
+ editor = new Editor();
+ instanceSpy = jest.spyOn(monacoEditor, 'create');
+ });
+
+ afterEach(() => {
+ editor.dispose();
+ });
+
+ it('can initialize several instances of the same editor', () => {
+ editor.createInstance(inst1Args);
+ expect(editor.instances).toHaveLength(1);
+
+ editor.createInstance(inst2Args);
+
+ expect(instanceSpy).toHaveBeenCalledTimes(2);
+ expect(editor.instances).toHaveLength(2);
+ });
+
+ it('sets independent models on independent instances', () => {
+ inst1 = editor.createInstance(inst1Args);
+ inst2 = editor.createInstance(inst2Args);
+
+ const model1 = inst1.getModel();
+ const model2 = inst2.getModel();
+
+ expect(model1).toBeDefined();
+ expect(model2).toBeDefined();
+ expect(model1).not.toEqual(model2);
+ });
+
+ it('shares global editor options among all instances', () => {
+ editor = new Editor({
+ readOnly: true,
+ });
+
+ inst1 = editor.createInstance(inst1Args);
+ expect(inst1.getOption(readOnlyIndex)).toBe(true);
+
+ inst2 = editor.createInstance(inst2Args);
+ expect(inst2.getOption(readOnlyIndex)).toBe(true);
+ });
+
+ it('allows overriding editor options on the instance level', () => {
+ editor = new Editor({
+ readOnly: true,
+ });
+ inst1 = editor.createInstance({
+ ...inst1Args,
+ readOnly: false,
+ });
+
+ expect(inst1.getOption(readOnlyIndex)).toBe(false);
+ });
+
+ it('disposes instances and relevant models independently from each other', () => {
+ inst1 = editor.createInstance(inst1Args);
+ inst2 = editor.createInstance(inst2Args);
+
+ expect(inst1.getModel()).not.toBe(null);
+ expect(inst2.getModel()).not.toBe(null);
+ expect(editor.instances).toHaveLength(2);
+ expect(monacoEditor.getModels()).toHaveLength(2);
+
+ inst1.dispose();
+ expect(inst1.getModel()).toBe(null);
+ expect(inst2.getModel()).not.toBe(null);
+ expect(editor.instances).toHaveLength(1);
+ expect(monacoEditor.getModels()).toHaveLength(1);
+ });
});
describe('implementation', () => {
+ let instance;
beforeEach(() => {
- editor.createInstance({ el: editorEl, blobPath, blobContent });
+ instance = editor.createInstance({ el: editorEl, blobPath, blobContent });
});
it('correctly proxies value from the model', () => {
- expect(editor.getValue()).toEqual(blobContent);
+ expect(instance.getValue()).toBe(blobContent);
});
it('is capable of changing the language of the model', () => {
@@ -108,24 +222,25 @@ describe('Base editor', () => {
const blobRenamedPath = 'test.js';
- expect(editor.model.getLanguageIdentifier().language).toEqual('markdown');
- editor.updateModelLanguage(blobRenamedPath);
+ expect(instance.getModel().getLanguageIdentifier().language).toBe('markdown');
+ instance.updateModelLanguage(blobRenamedPath);
- expect(editor.model.getLanguageIdentifier().language).toEqual('javascript');
+ expect(instance.getModel().getLanguageIdentifier().language).toBe('javascript');
});
it('falls back to plaintext if there is no language associated with an extension', () => {
const blobRenamedPath = 'test.myext';
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
- editor.updateModelLanguage(blobRenamedPath);
+ instance.updateModelLanguage(blobRenamedPath);
expect(spy).not.toHaveBeenCalled();
- expect(editor.model.getLanguageIdentifier().language).toEqual('plaintext');
+ expect(instance.getModel().getLanguageIdentifier().language).toBe('plaintext');
});
});
describe('extensions', () => {
+ let instance;
const foo1 = jest.fn();
const foo2 = jest.fn();
const bar = jest.fn();
@@ -139,14 +254,14 @@ describe('Base editor', () => {
foo: foo2,
};
beforeEach(() => {
- editor.createInstance({ el: editorEl, blobPath, blobContent });
+ instance = editor.createInstance({ el: editorEl, blobPath, blobContent });
});
it('is extensible with the extensions', () => {
- expect(editor.foo).toBeUndefined();
+ expect(instance.foo).toBeUndefined();
editor.use(MyExt1);
- expect(editor.foo).toEqual(foo1);
+ expect(instance.foo).toEqual(foo1);
});
it('does not fail if no extensions supplied', () => {
@@ -157,18 +272,18 @@ describe('Base editor', () => {
});
it('is extensible with multiple extensions', () => {
- expect(editor.foo).toBeUndefined();
- expect(editor.bar).toBeUndefined();
+ expect(instance.foo).toBeUndefined();
+ expect(instance.bar).toBeUndefined();
editor.use([MyExt1, MyExt2]);
- expect(editor.foo).toEqual(foo1);
- expect(editor.bar).toEqual(bar);
+ expect(instance.foo).toEqual(foo1);
+ expect(instance.bar).toEqual(bar);
});
it('uses the last definition of a method in case of an overlap', () => {
editor.use([MyExt1, MyExt2, MyExt3]);
- expect(editor).toEqual(
+ expect(instance).toEqual(
expect.objectContaining({
foo: foo2,
bar,
@@ -179,15 +294,47 @@ describe('Base editor', () => {
it('correctly resolves references withing extensions', () => {
const FunctionExt = {
inst() {
- return this.instance;
+ return this;
},
mod() {
- return this.model;
+ return this.getModel();
},
};
editor.use(FunctionExt);
- expect(editor.inst()).toEqual(editor.instance);
- expect(editor.mod()).toEqual(editor.model);
+ expect(instance.inst()).toEqual(editor.instances[0]);
+ });
+
+ describe('multiple instances', () => {
+ let inst1;
+ let inst2;
+ let editorEl1;
+ let editorEl2;
+
+ beforeEach(() => {
+ setFixtures('<div id="editor1"></div><div id="editor2"></div>');
+ editorEl1 = document.getElementById('editor1');
+ editorEl2 = document.getElementById('editor2');
+ inst1 = editor.createInstance({ el: editorEl1, blobPath: `foo-${blobPath}` });
+ inst2 = editor.createInstance({ el: editorEl2, blobPath: `bar-${blobPath}` });
+ });
+
+ afterEach(() => {
+ editor.dispose();
+ editorEl1.remove();
+ editorEl2.remove();
+ });
+
+ it('extends all instances if no specific instance is passed', () => {
+ editor.use(MyExt1);
+ expect(inst1.foo).toEqual(foo1);
+ expect(inst2.foo).toEqual(foo1);
+ });
+
+ it('extends specific instance if it has been passed', () => {
+ editor.use(MyExt1, inst2);
+ expect(inst1.foo).toBeUndefined();
+ expect(inst2.foo).toEqual(foo1);
+ });
});
});
diff --git a/spec/frontend/editor/editor_markdown_ext_spec.js b/spec/frontend/editor/editor_markdown_ext_spec.js
index b0fabad8542..30ab29aad35 100644
--- a/spec/frontend/editor/editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/editor_markdown_ext_spec.js
@@ -4,6 +4,7 @@ import EditorMarkdownExtension from '~/editor/editor_markdown_ext';
describe('Markdown Extension for Editor Lite', () => {
let editor;
+ let instance;
let editorEl;
const firstLine = 'This is a';
const secondLine = 'multiline';
@@ -13,19 +14,19 @@ describe('Markdown Extension for Editor Lite', () => {
const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => {
const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
- editor.instance.setSelection(selection);
+ instance.setSelection(selection);
};
const selectSecondString = () => setSelection(2, 1, 2, secondLine.length + 1); // select the whole second line
const selectSecondAndThirdLines = () => setSelection(2, 1, 3, thirdLine.length + 1); // select second and third lines
- const selectionToString = () => editor.instance.getSelection().toString();
- const positionToString = () => editor.instance.getPosition().toString();
+ const selectionToString = () => instance.getSelection().toString();
+ const positionToString = () => instance.getPosition().toString();
beforeEach(() => {
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
editor = new EditorLite();
- editor.createInstance({
+ instance = editor.createInstance({
el: editorEl,
blobPath: filePath,
blobContent: text,
@@ -34,17 +35,16 @@ describe('Markdown Extension for Editor Lite', () => {
});
afterEach(() => {
- editor.instance.dispose();
- editor.model.dispose();
+ instance.dispose();
editorEl.remove();
});
describe('getSelectedText', () => {
it('does not fail if there is no selection and returns the empty string', () => {
- jest.spyOn(editor.instance, 'getSelection');
- const resText = editor.getSelectedText();
+ jest.spyOn(instance, 'getSelection');
+ const resText = instance.getSelectedText();
- expect(editor.instance.getSelection).toHaveBeenCalled();
+ expect(instance.getSelection).toHaveBeenCalled();
expect(resText).toBe('');
});
@@ -56,7 +56,7 @@ describe('Markdown Extension for Editor Lite', () => {
`('correctly returns selected text for $description', ({ selection, expectedString }) => {
setSelection(...selection);
- const resText = editor.getSelectedText();
+ const resText = instance.getSelectedText();
expect(resText).toBe(expectedString);
});
@@ -65,7 +65,7 @@ describe('Markdown Extension for Editor Lite', () => {
selectSecondString();
const firstLineSelection = new Range(1, 1, 1, firstLine.length + 1);
- const resText = editor.getSelectedText(firstLineSelection);
+ const resText = instance.getSelectedText(firstLineSelection);
expect(resText).toBe(firstLine);
});
@@ -76,25 +76,25 @@ describe('Markdown Extension for Editor Lite', () => {
it('replaces selected text with the supplied one', () => {
selectSecondString();
- editor.replaceSelectedText(expectedStr);
+ instance.replaceSelectedText(expectedStr);
- expect(editor.getValue()).toBe(`${firstLine}\n${expectedStr}\n${thirdLine}`);
+ expect(instance.getValue()).toBe(`${firstLine}\n${expectedStr}\n${thirdLine}`);
});
it('prepends the supplied text if no text is selected', () => {
- editor.replaceSelectedText(expectedStr);
- expect(editor.getValue()).toBe(`${expectedStr}${firstLine}\n${secondLine}\n${thirdLine}`);
+ instance.replaceSelectedText(expectedStr);
+ expect(instance.getValue()).toBe(`${expectedStr}${firstLine}\n${secondLine}\n${thirdLine}`);
});
it('replaces selection with empty string if no text is supplied', () => {
selectSecondString();
- editor.replaceSelectedText();
- expect(editor.getValue()).toBe(`${firstLine}\n\n${thirdLine}`);
+ instance.replaceSelectedText();
+ expect(instance.getValue()).toBe(`${firstLine}\n\n${thirdLine}`);
});
it('puts cursor at the end of the new string and collapses selection by default', () => {
selectSecondString();
- editor.replaceSelectedText(expectedStr);
+ instance.replaceSelectedText(expectedStr);
expect(positionToString()).toBe(`(2,${expectedStr.length + 1})`);
expect(selectionToString()).toBe(
@@ -106,7 +106,7 @@ describe('Markdown Extension for Editor Lite', () => {
const select = 'url';
const complexReplacementString = `[${secondLine}](${select})`;
selectSecondString();
- editor.replaceSelectedText(complexReplacementString, select);
+ instance.replaceSelectedText(complexReplacementString, select);
expect(positionToString()).toBe(`(2,${complexReplacementString.length + 1})`);
expect(selectionToString()).toBe(`[2,1 -> 2,${complexReplacementString.length + 1}]`);
@@ -116,7 +116,7 @@ describe('Markdown Extension for Editor Lite', () => {
describe('moveCursor', () => {
const setPosition = endCol => {
const currentPos = new Position(2, endCol);
- editor.instance.setPosition(currentPos);
+ instance.setPosition(currentPos);
};
it.each`
@@ -136,9 +136,9 @@ describe('Markdown Extension for Editor Lite', () => {
({ startColumn, shift, endPosition }) => {
setPosition(startColumn);
if (Array.isArray(shift)) {
- editor.moveCursor(...shift);
+ instance.moveCursor(...shift);
} else {
- editor.moveCursor(shift);
+ instance.moveCursor(shift);
}
expect(positionToString()).toBe(endPosition);
},
@@ -153,18 +153,18 @@ describe('Markdown Extension for Editor Lite', () => {
expect(selectionToString()).toBe(`[2,1 -> 3,${thirdLine.length + 1}]`);
- editor.selectWithinSelection(toSelect, selectedText);
+ instance.selectWithinSelection(toSelect, selectedText);
expect(selectionToString()).toBe(`[3,1 -> 3,${toSelect.length + 1}]`);
});
it('does not fail when only `toSelect` is supplied and fetches the text from selection', () => {
- jest.spyOn(editor, 'getSelectedText');
+ jest.spyOn(instance, 'getSelectedText');
const toSelect = 'string';
selectSecondAndThirdLines();
- editor.selectWithinSelection(toSelect);
+ instance.selectWithinSelection(toSelect);
- expect(editor.getSelectedText).toHaveBeenCalled();
+ expect(instance.getSelectedText).toHaveBeenCalled();
expect(selectionToString()).toBe(`[3,1 -> 3,${toSelect.length + 1}]`);
});
@@ -176,7 +176,7 @@ describe('Markdown Extension for Editor Lite', () => {
expect(positionToString()).toBe(expectedPos);
expect(selectionToString()).toBe(expectedSelection);
- editor.selectWithinSelection();
+ instance.selectWithinSelection();
expect(positionToString()).toBe(expectedPos);
expect(selectionToString()).toBe(expectedSelection);
@@ -190,12 +190,12 @@ describe('Markdown Extension for Editor Lite', () => {
expect(positionToString()).toBe(expectedPos);
expect(selectionToString()).toBe(expectedSelection);
- editor.selectWithinSelection(toSelect);
+ instance.selectWithinSelection(toSelect);
expect(positionToString()).toBe(expectedPos);
expect(selectionToString()).toBe(expectedSelection);
- editor.selectWithinSelection();
+ instance.selectWithinSelection();
expect(positionToString()).toBe(expectedPos);
expect(selectionToString()).toBe(expectedSelection);
diff --git a/spec/frontend/emoji/emoji_spec.js b/spec/frontend/emoji/emoji_spec.js
index 9b49c8b8ab5..53c6d0835bc 100644
--- a/spec/frontend/emoji/emoji_spec.js
+++ b/spec/frontend/emoji/emoji_spec.js
@@ -55,11 +55,10 @@ const emojiFixtureMap = {
describe('gl_emoji', () => {
let mock;
- const emojiData = getJSONFixture('emojis/emojis.json');
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
+ mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200);
return initEmojiMap().catch(() => {});
});
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index e7f5ee4bc4d..ebdc4923045 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,9 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import eventHub from '~/environments/event_hub';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
-import Icon from '~/vue_shared/components/icon.vue';
describe('EnvironmentActions Component', () => {
let vm;
@@ -17,7 +16,7 @@ describe('EnvironmentActions Component', () => {
});
it('should render a dropdown button with 2 icons', () => {
- expect(vm.find('.dropdown-new').findAll(Icon).length).toBe(2);
+ expect(vm.find('.dropdown-new').findAll(GlIcon).length).toBe(2);
});
it('should render a dropdown button with aria-label description', () => {
@@ -60,11 +59,7 @@ describe('EnvironmentActions Component', () => {
});
it("should render a disabled action when it's not playable", () => {
- expect(vm.find('.dropdown-menu li:last-child button').attributes('disabled')).toEqual(
- 'disabled',
- );
-
- expect(vm.find('.dropdown-menu li:last-child button').classes('disabled')).toBe(true);
+ expect(vm.find('.dropdown-menu li:last-child gl-button-stub').props('disabled')).toBe(true);
});
});
@@ -82,7 +77,7 @@ describe('EnvironmentActions Component', () => {
scheduledAt: '2018-10-05T08:23:00Z',
};
const findDropdownItem = action => {
- const buttons = vm.findAll('.dropdown-menu li button');
+ const buttons = vm.findAll('.dropdown-menu li gl-button-stub');
return buttons.filter(button => button.text().startsWith(action.name)).at(0);
};
@@ -96,7 +91,7 @@ describe('EnvironmentActions Component', () => {
eventHub.$on('postAction', emitSpy);
jest.spyOn(window, 'confirm').mockImplementation(() => true);
- findDropdownItem(scheduledJobAction).trigger('click');
+ findDropdownItem(scheduledJobAction).vm.$emit('click');
expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
@@ -107,7 +102,7 @@ describe('EnvironmentActions Component', () => {
eventHub.$on('postAction', emitSpy);
jest.spyOn(window, 'confirm').mockImplementation(() => false);
- findDropdownItem(scheduledJobAction).trigger('click');
+ findDropdownItem(scheduledJobAction).vm.$emit('click');
expect(window.confirm).toHaveBeenCalled();
expect(emitSpy).not.toHaveBeenCalled();
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index 5d374a162ab..1b429783821 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -3,7 +3,7 @@ import { format } from 'timeago.js';
import EnvironmentItem from '~/environments/components/environment_item.vue';
import PinComponent from '~/environments/components/environment_pin.vue';
import DeleteComponent from '~/environments/components/environment_delete.vue';
-
+import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import { environment, folder, tableData } from './mock_data';
describe('Environment item', () => {
@@ -135,7 +135,7 @@ describe('Environment item', () => {
});
describe('in the past', () => {
- const pastDate = new Date(Date.now() - 100000);
+ const pastDate = new Date(differenceInMilliseconds(100000));
beforeEach(() => {
factory({
propsData: {
diff --git a/spec/frontend/environments/environment_monitoring_spec.js b/spec/frontend/environments/environment_monitoring_spec.js
index d2129bd7b30..a73f49f1047 100644
--- a/spec/frontend/environments/environment_monitoring_spec.js
+++ b/spec/frontend/environments/environment_monitoring_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import MonitoringComponent from '~/environments/components/environment_monitoring.vue';
-import Icon from '~/vue_shared/components/icon.vue';
describe('Monitoring Component', () => {
let wrapper;
@@ -15,8 +15,8 @@ describe('Monitoring Component', () => {
});
};
- const findIcons = () => wrapper.findAll(Icon);
- const findIconsByName = name => findIcons().filter(icon => icon.props('name') === name);
+ const findButtons = () => wrapper.findAll(GlButton);
+ const findButtonsByIcon = icon => findButtons().filter(button => button.props('icon') === icon);
beforeEach(() => {
createWrapper();
@@ -30,7 +30,7 @@ describe('Monitoring Component', () => {
it('should render a link to environment monitoring page', () => {
expect(wrapper.attributes('href')).toEqual(monitoringUrl);
- expect(findIconsByName('chart').length).toBe(1);
+ expect(findButtonsByIcon('chart').length).toBe(1);
expect(wrapper.attributes('title')).toBe('Monitoring');
expect(wrapper.attributes('aria-label')).toBe('Monitoring');
});
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index 486f6db7366..f48091adb44 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlIcon } from '@gitlab/ui';
import eventHub from '~/environments/event_hub';
import PinComponent from '~/environments/components/environment_pin.vue';
@@ -32,12 +31,12 @@ describe('Pin Component', () => {
});
it('should render the component with thumbtack icon', () => {
- expect(wrapper.find(Icon).props('name')).toBe('thumbtack');
+ expect(wrapper.find(GlIcon).props('name')).toBe('thumbtack');
});
it('should emit onPinClick when clicked', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const button = wrapper.find(GlDeprecatedButton);
+ const button = wrapper.find(GlButton);
button.vm.$emit('click');
diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index f25e05f9cd8..fb62a096c3d 100644
--- a/spec/frontend/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import eventHub from '~/environments/event_hub';
import RollbackComponent from '~/environments/components/environment_rollback.vue';
@@ -40,7 +40,7 @@ describe('Rollback Component', () => {
},
},
});
- const button = wrapper.find(GlDeprecatedButton);
+ const button = wrapper.find(GlButton);
button.vm.$emit('click');
diff --git a/spec/frontend/environments/environment_terminal_button_spec.js b/spec/frontend/environments/environment_terminal_button_spec.js
index 007fda2f2cc..274186fbbd6 100644
--- a/spec/frontend/environments/environment_terminal_button_spec.js
+++ b/spec/frontend/environments/environment_terminal_button_spec.js
@@ -22,7 +22,7 @@ describe('Stop Component', () => {
});
it('should render a link to open a web terminal with the provided path', () => {
- expect(wrapper.is('a')).toBe(true);
+ expect(wrapper.element.tagName).toBe('A');
expect(wrapper.attributes('title')).toBe('Terminal');
expect(wrapper.attributes('aria-label')).toBe('Terminal');
expect(wrapper.attributes('href')).toBe(terminalPath);
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index d440bf73e15..fe32bf918dd 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -144,16 +144,16 @@ describe('Environment', () => {
});
it('should open a closed folder', () => {
- expect(wrapper.find('.folder-icon.ic-chevron-right').exists()).toBe(false);
+ expect(wrapper.find('.folder-icon[data-testid="chevron-right-icon"]').exists()).toBe(false);
});
it('should close an opened folder', () => {
- expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(true);
+ expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(true);
// close folder
wrapper.find('.folder-name').trigger('click');
wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(false);
+ expect(wrapper.find('.folder-icon[data-testid="chevron-down-icon"]').exists()).toBe(false);
});
});
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 bad70a31599..31f355ce6f1 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -163,7 +163,7 @@ describe('ErrorTrackingList', () => {
it('each error in the list should have an action button set', () => {
findErrorListRows().wrappers.forEach(row => {
- expect(row.contains(ErrorTrackingActions)).toBe(true);
+ expect(row.find(ErrorTrackingActions).exists()).toBe(true);
});
});
@@ -259,23 +259,15 @@ describe('ErrorTrackingList', () => {
errorId: errorsList[0].id,
status: 'ignored',
});
- expect(actions.updateStatus).toHaveBeenCalledWith(
- expect.anything(),
- {
- endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
- status: 'ignored',
- },
- undefined,
- );
+ expect(actions.updateStatus).toHaveBeenCalledWith(expect.anything(), {
+ endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
+ status: 'ignored',
+ });
});
it('calls an action to remove the item from the list', () => {
findErrorActions().vm.$emit('update-issue-status', { errorId: '1', status: undefined });
- expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
- expect.anything(),
- '1',
- undefined,
- );
+ expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(expect.anything(), '1');
});
});
@@ -298,23 +290,15 @@ describe('ErrorTrackingList', () => {
errorId: errorsList[0].id,
status: 'resolved',
});
- expect(actions.updateStatus).toHaveBeenCalledWith(
- expect.anything(),
- {
- endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
- status: 'resolved',
- },
- undefined,
- );
+ expect(actions.updateStatus).toHaveBeenCalledWith(expect.anything(), {
+ endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
+ status: 'resolved',
+ });
});
it('calls an action to remove the item from the list', () => {
findErrorActions().vm.$emit('update-issue-status', { errorId: '1', status: undefined });
- expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
- expect.anything(),
- '1',
- undefined,
- );
+ expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(expect.anything(), '1');
});
});
@@ -443,7 +427,6 @@ describe('ErrorTrackingList', () => {
expect(actions.fetchPaginatedResults).toHaveBeenLastCalledWith(
expect.anything(),
'previousCursor',
- undefined,
);
});
});
@@ -462,7 +445,6 @@ describe('ErrorTrackingList', () => {
expect(actions.fetchPaginatedResults).toHaveBeenLastCalledWith(
expect.anything(),
'nextCursor',
- undefined,
);
});
});
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index df7bff201f1..6df25ad6819 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -1,10 +1,9 @@
import { shallowMount } from '@vue/test-utils';
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
describe('Stacktrace Entry', () => {
let wrapper;
@@ -39,7 +38,7 @@ describe('Stacktrace Entry', () => {
mountComponent({ lines });
expect(wrapper.find(StackTraceEntry).exists()).toBe(true);
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
- expect(wrapper.find(Icon).exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
expect(wrapper.find(FileIcon).exists()).toBe(true);
expect(wrapper.find('table').exists()).toBe(false);
});
@@ -57,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(Icon).exists()).toBe(false);
+ expect(wrapper.find(GlIcon).exists()).toBe(false);
expect(trimText(findFileHeaderContent())).toContain(
`in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`,
);
diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb
index fcd68662acc..40f613a9422 100644
--- a/spec/frontend/fixtures/search.rb
+++ b/spec/frontend/fixtures/search.rb
@@ -7,10 +7,16 @@ RSpec.describe SearchController, '(JavaScript fixtures)', type: :controller do
render_views
+ let_it_be(:user) { create(:admin) }
+
before(:all) do
clean_frontend_fixtures('search/')
end
+ before do
+ sign_in(user)
+ end
+
it 'search/show.html' do
get :show
diff --git a/spec/frontend/fixtures/static/ajax_loading_spinner.html b/spec/frontend/fixtures/static/ajax_loading_spinner.html
deleted file mode 100644
index 0e1ebb32b1c..00000000000
--- a/spec/frontend/fixtures/static/ajax_loading_spinner.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami">
-<i class="fa fa-trash-o"></i>
-</a>
diff --git a/spec/frontend/fixtures/static/deprecated_jquery_dropdown.html b/spec/frontend/fixtures/static/deprecated_jquery_dropdown.html
new file mode 100644
index 00000000000..3db9bafcb9f
--- /dev/null
+++ b/spec/frontend/fixtures/static/deprecated_jquery_dropdown.html
@@ -0,0 +1,39 @@
+<div>
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ data-toggle="dropdown"
+ id="js-project-dropdown"
+ type="button"
+ >
+ <div class="dropdown-toggle-text">
+ Projects
+ </div>
+ <i class="fa fa-chevron-down dropdown-toggle-caret js-projects-dropdown-toggle"></i>
+ </button>
+ <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
+ <div class="dropdown-title gl-display-flex gl-align-items-center">
+ <span class="gl-ml-auto">Go to project</span>
+ <button
+ aria-label="Close"
+ type="button"
+ class="btn dropdown-title-button dropdown-menu-close gl-ml-auto btn-default btn-sm gl-button btn-default-tertiary btn-icon"
+ >
+ <svg data-testid="close-icon" class="gl-icon s16 dropdown-menu-close-icon">
+ <use
+ href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#close"
+ ></use>
+ </svg>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input class="dropdown-input-field" placeholder="Filter results" type="search" />
+ <i class="fa fa-search dropdown-input-search"></i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/spec/frontend/fixtures/static/gl_dropdown.html b/spec/frontend/fixtures/static/gl_dropdown.html
deleted file mode 100644
index 08f6738414e..00000000000
--- a/spec/frontend/fixtures/static/gl_dropdown.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<div>
-<div class="dropdown inline">
-<button class="dropdown-menu-toggle" data-toggle="dropdown" id="js-project-dropdown" type="button">
-<div class="dropdown-toggle-text">
-Projects
-</div>
-<i class="fa fa-chevron-down dropdown-toggle-caret js-projects-dropdown-toggle"></i>
-</button>
-<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
-<div class="dropdown-title">
-<span>Go to project</span>
-<button aria="{:label=&gt;&quot;Close&quot;}" class="dropdown-title-button dropdown-menu-close">
-<i class="fa fa-times dropdown-menu-close-icon"></i>
-</button>
-</div>
-<div class="dropdown-input">
-<input class="dropdown-input-field" placeholder="Filter results" type="search">
-<i class="fa fa-search dropdown-input-search"></i>
-</div>
-<div class="dropdown-content"></div>
-<div class="dropdown-loading">
-<i class="fa fa-spinner fa-spin"></i>
-</div>
-</div>
-</div>
-</div>
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
index be3874d7c42..a6a8ba7318b 100644
--- a/spec/frontend/fixtures/u2f.rb
+++ b/spec/frontend/fixtures/u2f.rb
@@ -11,6 +11,10 @@ RSpec.context 'U2F' do
clean_frontend_fixtures('u2f/')
end
+ before do
+ stub_feature_flags(webauthn: false)
+ end
+
describe SessionsController, '(JavaScript fixtures)', type: :controller do
include DeviseHelpers
diff --git a/spec/frontend/fixtures/webauthn.rb b/spec/frontend/fixtures/webauthn.rb
new file mode 100644
index 00000000000..b195fee76f0
--- /dev/null
+++ b/spec/frontend/fixtures/webauthn.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.context 'WebAuthn' do
+ include JavaScriptFixturesHelpers
+
+ let(:user) { create(:user, :two_factor_via_webauthn, otp_secret: 'otpsecret:coolkids') }
+
+ before(:all) do
+ clean_frontend_fixtures('webauthn/')
+ end
+
+ describe SessionsController, '(JavaScript fixtures)', type: :controller do
+ include DeviseHelpers
+
+ render_views
+
+ before do
+ set_devise_mapping(context: @request)
+ end
+
+ it 'webauthn/authenticate.html' do
+ allow(controller).to receive(:find_user).and_return(user)
+ post :create, params: { user: { login: user.username, password: user.password } }
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before do
+ sign_in(user)
+ allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance|
+ allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
+ end
+ end
+
+ it 'webauthn/register.html' do
+ get :show
+
+ expect(response).to be_successful
+ end
+ end
+end
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index 204bbfb9c2f..c5155315bb9 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -69,8 +69,8 @@ describe('FrequentItemsSearchInputComponent', () => {
describe('template', () => {
it('should render component element', () => {
expect(wrapper.classes()).toContain('search-input-container');
- expect(wrapper.contains('input.form-control')).toBe(true);
- expect(wrapper.contains('.search-icon')).toBe(true);
+ expect(wrapper.find('input.form-control').exists()).toBe(true);
+ expect(wrapper.find('.search-icon').exists()).toBe(true);
expect(wrapper.find('input.form-control').attributes('placeholder')).toBe(
'Search your projects',
);
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 869347128e5..6c40b1ba3a7 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,11 +1,9 @@
/* eslint no-param-reassign: "off" */
import $ from 'jquery';
+import '~/lib/utils/jquery_at_who';
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
-import 'jquery.caret';
-import '@gitlab/at.js';
-
import { TEST_HOST } from 'helpers/test_constants';
import { getJSONFixture } from 'helpers/fixtures';
@@ -121,7 +119,7 @@ describe('GfmAutoComplete', () => {
const defaultMatcher = (context, flag, subtext) =>
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext);
- const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$'];
+ const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$', '+'];
const otherFlags = ['/', ':'];
const flags = flagsUseDefaultMatcher.concat(otherFlags);
@@ -155,7 +153,6 @@ describe('GfmAutoComplete', () => {
'я',
'.',
"'",
- '+',
'-',
'_',
];
diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js
index 809cc5c88e2..644b687aa19 100644
--- a/spec/frontend/gpg_badges_spec.js
+++ b/spec/frontend/gpg_badges_spec.js
@@ -68,7 +68,7 @@ describe('GpgBadges', () => {
GpgBadges.fetch()
.then(() => {
expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null);
- const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin');
+ const spinners = document.querySelectorAll('.js-loading-gpg-badge span.gl-spinner');
expect(spinners.length).toBe(1);
done();
diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
index 0e16b726c4b..0befe1aa192 100644
--- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
+++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
@@ -83,7 +83,7 @@ exports[`grafana integration component default state to match the default snapsh
More information
- <icon-stub
+ <gl-icon-stub
class="vertical-align-middle"
name="external-link"
size="16"
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
new file mode 100644
index 00000000000..95003b211fd
--- /dev/null
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -0,0 +1,144 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlBanner } from '@gitlab/ui';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
+import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+
+jest.mock('~/lib/utils/common_utils');
+
+const isDismissedKey = 'invite_99_1';
+const title = 'Collaborate with your team';
+const body =
+ "We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge";
+const svgPath = '/illustrations/background';
+const inviteMembersPath = 'groups/members';
+const buttonText = 'Invite your colleagues';
+const trackLabel = 'invite_members_banner';
+
+const createComponent = (stubs = {}) => {
+ return shallowMount(InviteMembersBanner, {
+ provide: {
+ svgPath,
+ inviteMembersPath,
+ isDismissedKey,
+ trackLabel,
+ },
+ stubs,
+ });
+};
+
+describe('InviteMembersBanner', () => {
+ let wrapper;
+ let trackingSpy;
+
+ beforeEach(() => {
+ document.body.dataset.page = 'any:page';
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ unmockTracking();
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ GlBanner });
+ });
+
+ const trackCategory = undefined;
+ const displayEvent = 'invite_members_banner_displayed';
+ const buttonClickEvent = 'invite_members_banner_button_clicked';
+ const dismissEvent = 'invite_members_banner_dismissed';
+
+ it('sends the displayEvent when the banner is displayed', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(trackCategory, displayEvent, {
+ label: trackLabel,
+ });
+ });
+
+ it('sets the button attributes for the buttonClickEvent', () => {
+ const button = wrapper.find(`[href='${wrapper.vm.inviteMembersPath}']`);
+
+ expect(button.attributes()).toMatchObject({
+ 'data-track-event': buttonClickEvent,
+ 'data-track-label': trackLabel,
+ });
+ });
+
+ it('sends the dismissEvent when the banner is dismissed', () => {
+ wrapper.find(GlBanner).vm.$emit('close');
+
+ expect(trackingSpy).toHaveBeenCalledWith(trackCategory, dismissEvent, {
+ label: trackLabel,
+ });
+ });
+ });
+
+ describe('rendering', () => {
+ const findBanner = () => {
+ return wrapper.find(GlBanner);
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('uses the svgPath for the banner svgpath', () => {
+ expect(findBanner().attributes('svgpath')).toBe(svgPath);
+ });
+
+ it('uses the title from options for title', () => {
+ expect(findBanner().attributes('title')).toBe(title);
+ });
+
+ it('includes the body text from options', () => {
+ expect(findBanner().html()).toContain(body);
+ });
+
+ it('uses the button_text text from options for buttontext', () => {
+ expect(findBanner().attributes('buttontext')).toBe(buttonText);
+ });
+
+ it('uses the href from inviteMembersPath for buttonlink', () => {
+ expect(findBanner().attributes('buttonlink')).toBe(inviteMembersPath);
+ });
+ });
+
+ describe('dismissing', () => {
+ const findButton = () => {
+ return wrapper.find('button');
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent({ GlBanner });
+
+ findButton().trigger('click');
+ });
+
+ it('sets iDismissed to true', () => {
+ expect(wrapper.vm.isDismissed).toBe(true);
+ });
+
+ it('sets the cookie with the isDismissedKey', () => {
+ expect(setCookie).toHaveBeenCalledWith(isDismissedKey, true);
+ });
+ });
+
+ describe('when a dismiss cookie exists', () => {
+ beforeEach(() => {
+ parseBoolean.mockReturnValue(true);
+
+ wrapper = createComponent({ GlBanner });
+ });
+
+ it('sets isDismissed to true', () => {
+ expect(wrapper.vm.isDismissed).toBe(true);
+ });
+
+ it('does not render the banner', () => {
+ expect(wrapper.contains(GlBanner)).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js
index c0dc1a816e6..f5df8c180d5 100644
--- a/spec/frontend/groups/components/item_actions_spec.js
+++ b/spec/frontend/groups/components/item_actions_spec.js
@@ -57,8 +57,8 @@ describe('ItemActionsComponent', () => {
expect(editBtn.getAttribute('href')).toBe(group.editPath);
expect(editBtn.getAttribute('aria-label')).toBe('Edit group');
expect(editBtn.dataset.originalTitle).toBe('Edit group');
- expect(editBtn.querySelectorAll('svg use').length).not.toBe(0);
- expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings');
+ expect(editBtn.querySelectorAll('svg').length).not.toBe(0);
+ expect(editBtn.querySelector('svg').getAttribute('data-testid')).toBe('settings-icon');
newVm.$destroy();
});
@@ -75,8 +75,8 @@ describe('ItemActionsComponent', () => {
expect(leaveBtn.getAttribute('href')).toBe(group.leavePath);
expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group');
expect(leaveBtn.dataset.originalTitle).toBe('Leave this group');
- expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0);
- expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave');
+ expect(leaveBtn.querySelectorAll('svg').length).not.toBe(0);
+ expect(leaveBtn.querySelector('svg').getAttribute('data-testid')).toBe('leave-icon');
newVm.$destroy();
});
diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js
index bfe27be9b51..4ff7482414c 100644
--- a/spec/frontend/groups/components/item_caret_spec.js
+++ b/spec/frontend/groups/components/item_caret_spec.js
@@ -27,12 +27,12 @@ describe('ItemCaretComponent', () => {
it('should render caret down icon if `isGroupOpen` prop is `true`', () => {
vm = createComponent(true);
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-down');
+ expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('angle-down-icon');
});
it('should render caret right icon if `isGroupOpen` prop is `false`', () => {
vm = createComponent();
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-right');
+ expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('angle-right-icon');
});
});
});
diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js
index da6f145fa19..11246390444 100644
--- a/spec/frontend/groups/components/item_stats_value_spec.js
+++ b/spec/frontend/groups/components/item_stats_value_spec.js
@@ -72,7 +72,7 @@ describe('ItemStatsValueComponent', () => {
});
it('renders element icon correctly', () => {
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('folder');
+ expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('folder-icon');
});
it('renders value count correctly', () => {
diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js
index 251b5b5ff4c..477c413ddcd 100644
--- a/spec/frontend/groups/components/item_type_icon_spec.js
+++ b/spec/frontend/groups/components/item_type_icon_spec.js
@@ -27,12 +27,12 @@ describe('ItemTypeIconComponent', () => {
vm = createComponent(ITEM_TYPE.GROUP, true);
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder-open');
+ expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('folder-open-icon');
vm.$destroy();
vm = createComponent(ITEM_TYPE.GROUP);
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder');
+ expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('folder-o-icon');
vm.$destroy();
});
@@ -41,12 +41,12 @@ describe('ItemTypeIconComponent', () => {
vm = createComponent(ITEM_TYPE.PROJECT);
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('bookmark');
+ expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('bookmark-icon');
vm.$destroy();
vm = createComponent(ITEM_TYPE.GROUP);
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).not.toContain('bookmark');
+ expect(vm.$el.querySelector('svg').getAttribute('data-testid')).not.toBe('bookmark-icon');
vm.$destroy();
});
});
diff --git a/spec/frontend/groups/members/index_spec.js b/spec/frontend/groups/members/index_spec.js
new file mode 100644
index 00000000000..70fce0d60fb
--- /dev/null
+++ b/spec/frontend/groups/members/index_spec.js
@@ -0,0 +1,66 @@
+import { createWrapper } from '@vue/test-utils';
+import initGroupMembersApp from '~/groups/members';
+import GroupMembersApp from '~/groups/members/components/app.vue';
+import { membersJsonString, membersParsed } from './mock_data';
+
+describe('initGroupMembersApp', () => {
+ let el;
+ let vm;
+ let wrapper;
+
+ const setup = () => {
+ vm = initGroupMembersApp(el);
+ wrapper = createWrapper(vm);
+ };
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.setAttribute('data-members', membersJsonString);
+ el.setAttribute('data-group-id', '234');
+
+ window.gon = { current_user_id: 123 };
+
+ document.body.appendChild(el);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ el = null;
+
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders `GroupMembersApp`', () => {
+ setup();
+
+ expect(wrapper.find(GroupMembersApp).exists()).toBe(true);
+ });
+
+ it('sets `currentUserId` in Vuex store', () => {
+ setup();
+
+ expect(vm.$store.state.currentUserId).toBe(123);
+ });
+
+ describe('when `gon.current_user_id` is not set (user is not logged in)', () => {
+ it('sets `currentUserId` as `null` in Vuex store', () => {
+ window.gon = {};
+ setup();
+
+ expect(vm.$store.state.currentUserId).toBeNull();
+ });
+ });
+
+ it('parses and sets `data-group-id` as `sourceId` in Vuex store', () => {
+ setup();
+
+ expect(vm.$store.state.sourceId).toBe(234);
+ });
+
+ it('parses and sets `members` in Vuex store', () => {
+ setup();
+
+ expect(vm.$store.state.members).toEqual(membersParsed);
+ });
+});
diff --git a/spec/frontend/groups/members/mock_data.js b/spec/frontend/groups/members/mock_data.js
new file mode 100644
index 00000000000..b84c9c6d446
--- /dev/null
+++ b/spec/frontend/groups/members/mock_data.js
@@ -0,0 +1,33 @@
+export const membersJsonString =
+ '[{"requested_at":null,"can_update":true,"can_remove":true,"can_override":false,"access_level":{"integer_value":50,"string_value":"Owner"},"source":{"id":323,"name":"My group / my subgroup","web_url":"http://127.0.0.1:3000/groups/my-group/my-subgroup"},"user":{"id":1,"name":"Administrator","username":"root","web_url":"http://127.0.0.1:3000/root","avatar_url":"https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80\u0026d=identicon","blocked":false,"two_factor_enabled":false},"id":524,"created_at":"2020-08-21T21:33:27.631Z","expires_at":null,"using_license":false,"group_sso":false,"group_managed_account":false}]';
+
+export const membersParsed = [
+ {
+ requestedAt: null,
+ canUpdate: true,
+ canRemove: true,
+ canOverride: false,
+ accessLevel: { integerValue: 50, stringValue: 'Owner' },
+ source: {
+ id: 323,
+ name: 'My group / my subgroup',
+ webUrl: 'http://127.0.0.1:3000/groups/my-group/my-subgroup',
+ },
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
+ blocked: false,
+ twoFactorEnabled: false,
+ },
+ id: 524,
+ createdAt: '2020-08-21T21:33:27.631Z',
+ expiresAt: null,
+ usingLicense: false,
+ groupSso: false,
+ groupManagedAccount: false,
+ },
+];
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 59a8ca2ed23..27305abfafa 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -4,7 +4,7 @@ import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
describe('Header', () => {
describe('Todos notification', () => {
- const todosPendingCount = '.todos-count';
+ const todosPendingCount = '.js-todos-count';
const fixtureTemplate = 'issues/open-issue.html';
function isTodosCountHidden() {
diff --git a/spec/frontend/helpers/dom_events_helper.js b/spec/frontend/helpers/dom_events_helper.js
index 139e0813397..423e5c58bb4 100644
--- a/spec/frontend/helpers/dom_events_helper.js
+++ b/spec/frontend/helpers/dom_events_helper.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const triggerDOMEvent = type => {
window.document.dispatchEvent(
new Event(type, {
diff --git a/spec/frontend/helpers/fake_request_animation_frame.js b/spec/frontend/helpers/fake_request_animation_frame.js
index b01ae5b7c5f..f6fc29df4dc 100644
--- a/spec/frontend/helpers/fake_request_animation_frame.js
+++ b/spec/frontend/helpers/fake_request_animation_frame.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const useFakeRequestAnimationFrame = () => {
let orig;
diff --git a/spec/frontend/helpers/jest_helpers.js b/spec/frontend/helpers/jest_helpers.js
index 4a150be9935..0b623e0a59b 100644
--- a/spec/frontend/helpers/jest_helpers.js
+++ b/spec/frontend/helpers/jest_helpers.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
/*
@module
diff --git a/spec/frontend/helpers/local_storage_helper.js b/spec/frontend/helpers/local_storage_helper.js
index a66c31d1353..cd39b660bfd 100644
--- a/spec/frontend/helpers/local_storage_helper.js
+++ b/spec/frontend/helpers/local_storage_helper.js
@@ -10,7 +10,7 @@
*/
const useLocalStorage = fn => {
const origLocalStorage = window.localStorage;
- let currentLocalStorage;
+ let currentLocalStorage = origLocalStorage;
Object.defineProperty(window, 'localStorage', {
get: () => currentLocalStorage,
diff --git a/spec/frontend/helpers/local_storage_helper_spec.js b/spec/frontend/helpers/local_storage_helper_spec.js
index 18aec0f329a..6b44ea3a4c3 100644
--- a/spec/frontend/helpers/local_storage_helper_spec.js
+++ b/spec/frontend/helpers/local_storage_helper_spec.js
@@ -1,8 +1,15 @@
import { useLocalStorageSpy } from './local_storage_helper';
-useLocalStorageSpy();
+describe('block before helper is installed', () => {
+ it('should leave original localStorage intact', () => {
+ expect(localStorage.getItem).toEqual(expect.any(Function));
+ expect(jest.isMockFunction(localStorage.getItem)).toBe(false);
+ });
+});
describe('localStorage helper', () => {
+ useLocalStorageSpy();
+
it('mocks localStorage but works exactly like original localStorage', () => {
localStorage.setItem('test', 'testing');
localStorage.setItem('test2', 'testing');
diff --git a/spec/frontend/helpers/locale_helper.js b/spec/frontend/helpers/locale_helper.js
index 80047b06003..283d9bc14c9 100644
--- a/spec/frontend/helpers/locale_helper.js
+++ b/spec/frontend/helpers/locale_helper.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export const setLanguage = languageCode => {
const htmlElement = document.querySelector('html');
diff --git a/spec/frontend/helpers/mock_apollo_helper.js b/spec/frontend/helpers/mock_apollo_helper.js
new file mode 100644
index 00000000000..8a5a160231c
--- /dev/null
+++ b/spec/frontend/helpers/mock_apollo_helper.js
@@ -0,0 +1,23 @@
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import { createMockClient } from 'mock-apollo-client';
+import VueApollo from 'vue-apollo';
+
+export default (handlers = []) => {
+ const fragmentMatcher = { match: () => true };
+ const cache = new InMemoryCache({
+ fragmentMatcher,
+ addTypename: false,
+ });
+
+ const mockClient = createMockClient({ cache });
+
+ if (Array.isArray(handlers)) {
+ handlers.forEach(([query, value]) => mockClient.setRequestHandler(query, value));
+ } else {
+ throw new Error('You should pass an array of handlers to mock Apollo client');
+ }
+
+ const apolloProvider = new VueApollo({ defaultClient: mockClient });
+
+ return apolloProvider;
+};
diff --git a/spec/frontend/helpers/mock_dom_observer.js b/spec/frontend/helpers/mock_dom_observer.js
index 7aac51f6264..1b93b81535d 100644
--- a/spec/frontend/helpers/mock_dom_observer.js
+++ b/spec/frontend/helpers/mock_dom_observer.js
@@ -84,7 +84,9 @@ const useMockObserver = (key, createMock) => {
mockObserver.$_triggerObserve(...args);
};
- return { trigger };
+ const observersCount = () => mockObserver.$_observers.length;
+
+ return { trigger, observersCount };
};
export const useMockIntersectionObserver = () =>
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
new file mode 100644
index 00000000000..7b83f0aefca
--- /dev/null
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -0,0 +1,65 @@
+import { waitForCSSLoaded } from '../../../app/assets/javascripts/helpers/startup_css_helper';
+
+describe('waitForCSSLoaded', () => {
+ let mockedCallback;
+
+ beforeEach(() => {
+ mockedCallback = jest.fn();
+ });
+
+ describe('Promise-like api', () => {
+ it('can be used with a callback', async () => {
+ await waitForCSSLoaded(mockedCallback);
+ expect(mockedCallback).toHaveBeenCalledTimes(1);
+ });
+
+ it('can be used as a promise', async () => {
+ await waitForCSSLoaded().then(mockedCallback);
+ expect(mockedCallback).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('with startup css disabled', () => {
+ gon.features = {
+ startupCss: false,
+ };
+
+ it('should invoke the action right away', async () => {
+ const events = waitForCSSLoaded(mockedCallback);
+ await events;
+
+ expect(mockedCallback).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('with startup css enabled', () => {
+ gon.features = {
+ startupCss: true,
+ };
+
+ it('should dispatch CSSLoaded when the assets are cached or already loaded', async () => {
+ setFixtures(`
+ <link href="one.css" data-startupcss="loaded">
+ <link href="two.css" data-startupcss="loaded">
+ `);
+ await waitForCSSLoaded(mockedCallback);
+
+ expect(mockedCallback).toHaveBeenCalledTimes(1);
+ });
+
+ it('should wait to call CssLoaded until the assets are loaded', async () => {
+ setFixtures(`
+ <link href="one.css" data-startupcss="loading">
+ <link href="two.css" data-startupcss="loading">
+ `);
+ const events = waitForCSSLoaded(mockedCallback);
+ document
+ .querySelectorAll('[data-startupcss="loading"]')
+ .forEach(elem => elem.setAttribute('data-startupcss', 'loaded'));
+ document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
+ await events;
+
+ expect(mockedCallback).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/ide/commit_icon_spec.js b/spec/frontend/ide/commit_icon_spec.js
index e4a7394b089..0dfcae00298 100644
--- a/spec/frontend/ide/commit_icon_spec.js
+++ b/spec/frontend/ide/commit_icon_spec.js
@@ -7,7 +7,6 @@ const createFile = (name = 'name', id = name, type = '', parent = null) =>
id,
type,
icon: 'icon',
- url: 'url',
name,
path: parent ? `${parent.path}/${name}` : name,
parentPath: parent ? parent.path : '',
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js
index d8175025755..f1aa9187a8d 100644
--- a/spec/frontend/ide/components/branches/item_spec.js
+++ b/spec/frontend/ide/components/branches/item_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import { createStore } from '~/ide/stores';
import { createRouter } from '~/ide/ide_router';
import Item from '~/ide/components/branches/item.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { projectData } from '../../mock_data';
@@ -45,7 +45,7 @@ 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(Icon).exists()).toBe(false);
+ expect(wrapper.find(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(Icon).exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 9245cefc183..56667d6b03d 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -1,10 +1,13 @@
import Vue from 'vue';
+import { getByText } from '@testing-library/dom';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData } from 'jest/ide/mock_data';
import waitForPromises from 'helpers/wait_for_promises';
import { createStore } from '~/ide/stores';
+import consts from '~/ide/stores/modules/commit/constants';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
import { leftSidebarViews } from '~/ide/constants';
+import { createCodeownersCommitError, createUnexpectedCommitError } from '~/ide/lib/errors';
describe('IDE commit form', () => {
const Component = Vue.extend(CommitForm);
@@ -259,21 +262,47 @@ describe('IDE commit form', () => {
});
});
- it('opens new branch modal if commitChanges throws an error', () => {
- vm.commitChanges.mockRejectedValue({ success: false });
+ it.each`
+ createError | props
+ ${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
+ ${createUnexpectedCommitError} | ${{ actionPrimary: null }}
+ `('opens error modal if commitError with $error', async ({ createError, props }) => {
+ jest.spyOn(vm.$refs.commitErrorModal, 'show');
- jest.spyOn(vm.$refs.createBranchModal, 'show').mockImplementation();
+ const error = createError();
+ store.state.commit.commitError = error;
- return vm
- .$nextTick()
- .then(() => {
- vm.$el.querySelector('.btn-success').click();
+ await vm.$nextTick();
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.$refs.createBranchModal.show).toHaveBeenCalled();
- });
+ expect(vm.$refs.commitErrorModal.show).toHaveBeenCalled();
+ expect(vm.$refs.commitErrorModal).toMatchObject({
+ actionCancel: { text: 'Cancel' },
+ ...props,
+ });
+ // Because of the legacy 'mountComponent' approach here, the only way to
+ // test the text of the modal is by viewing the content of the modal added to the document.
+ expect(document.body).toHaveText(error.messageHTML);
+ });
+ });
+
+ describe('with error modal with primary', () => {
+ beforeEach(() => {
+ jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
+ });
+
+ it('updates commit action and commits', async () => {
+ store.state.commit.commitError = createCodeownersCommitError('test message');
+
+ await vm.$nextTick();
+
+ getByText(document.body, 'Create new branch').click();
+
+ await waitForPromises();
+
+ expect(vm.$store.dispatch.mock.calls).toEqual([
+ ['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
+ ['commit/commitChanges', undefined],
+ ]);
});
});
});
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
index 3a4dcc5873d..8b7e7da3b51 100644
--- a/spec/frontend/ide/components/error_message_spec.js
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -51,7 +51,7 @@ describe('IDE error message component', () => {
createComponent();
findDismissButton().trigger('click');
- expect(setErrorMessageMock).toHaveBeenCalledWith(expect.any(Object), null, undefined);
+ expect(setErrorMessageMock).toHaveBeenCalledWith(expect.any(Object), null);
});
describe('with action', () => {
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
index 4bd27d23f76..2a106ad37c0 100644
--- a/spec/frontend/ide/components/file_row_extra_spec.js
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -153,14 +153,14 @@ describe('IDE extra file row component', () => {
describe('merge request icon', () => {
it('hides when not a merge request change', () => {
- expect(vm.$el.querySelector('.ic-git-merge')).toBe(null);
+ expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).toBe(null);
});
it('shows when a merge request change', done => {
vm.file.mrChange = true;
vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ic-git-merge')).not.toBe(null);
+ expect(vm.$el.querySelector('[data-testid="git-merge-icon"]')).not.toBe(null);
done();
});
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index fed61233e55..bb8165d1a52 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -75,7 +75,8 @@ describe('ide/components/ide_status_list', () => {
describe('with binary file', () => {
beforeEach(() => {
- activeFile.binary = true;
+ activeFile.name = 'abc.dat';
+ activeFile.content = '🐱'; // non-ascii binary content
createComponent();
});
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
index dbfacb98813..a65d9e6f78b 100644
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -34,7 +34,7 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
</span>
</div>
- <icon-stub
+ <gl-icon-stub
class="ide-stage-collapse-icon"
name="angle-down"
size="16"
diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index babae00d2f7..5554738336a 100644
--- a/spec/frontend/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
@@ -23,6 +23,16 @@ describe('IDE job description', () => {
});
it('renders CI icon', () => {
- expect(vm.$el.querySelector('.ci-status-icon .ic-status_success_borderless')).not.toBe(null);
+ expect(
+ vm.$el.querySelector('.ci-status-icon [data-testid="status_success_borderless-icon"]'),
+ ).not.toBe(null);
+ });
+
+ it('renders bridge job details without the job link', () => {
+ vm = mountComponent(Component, {
+ job: { ...jobs[0], path: undefined },
+ });
+
+ expect(vm.$el.querySelector('[data-testid="description-detail-link"]')).toBe(null);
});
});
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 b8dbca97ade..42526590ebb 100644
--- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlIcon } from '@gitlab/ui';
import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue';
describe('IDE job log scroll button', () => {
@@ -27,7 +27,7 @@ describe('IDE job log scroll button', () => {
beforeEach(() => createComponent({ direction }));
it('returns proper icon name', () => {
- expect(wrapper.find(Icon).props('name')).toBe(icon);
+ expect(wrapper.find(GlIcon).props('name')).toBe(icon);
});
it('returns proper title', () => {
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
index acd30dee718..496d8284fdd 100644
--- a/spec/frontend/ide/components/jobs/detail_spec.js
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -24,7 +24,7 @@ describe('IDE jobs detail view', () => {
beforeEach(() => {
vm = createComponent();
- jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue();
+ jest.spyOn(vm, 'fetchJobLogs').mockResolvedValue();
});
afterEach(() => {
@@ -36,8 +36,8 @@ describe('IDE jobs detail view', () => {
vm = vm.$mount();
});
- it('calls fetchJobTrace', () => {
- expect(vm.fetchJobTrace).toHaveBeenCalled();
+ it('calls fetchJobLogs', () => {
+ expect(vm.fetchJobLogs).toHaveBeenCalled();
});
it('scrolls to bottom', () => {
@@ -96,7 +96,7 @@ describe('IDE jobs detail view', () => {
describe('scroll buttons', () => {
beforeEach(() => {
vm = createComponent();
- jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue();
+ jest.spyOn(vm, 'fetchJobLogs').mockResolvedValue();
});
afterEach(() => {
diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index 2f97d39e98e..93c01640b54 100644
--- a/spec/frontend/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
@@ -24,7 +24,7 @@ describe('IDE jobs item', () => {
});
it('renders CI icon', () => {
- expect(vm.$el.querySelector('.ic-status_success_borderless')).not.toBe(null);
+ expect(vm.$el.querySelector('[data-testid="status_success_borderless-icon"]')).not.toBe(null);
});
it('does not render view logs button if not started', done => {
diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js
index d8880fa7cb7..e821a585e18 100644
--- a/spec/frontend/ide/components/jobs/list_spec.js
+++ b/spec/frontend/ide/components/jobs/list_spec.js
@@ -99,11 +99,7 @@ describe('IDE stages list', () => {
it('calls toggleStageCollapsed when clicking stage header', () => {
findCardHeader().trigger('click');
- expect(storeActions.toggleStageCollapsed).toHaveBeenCalledWith(
- expect.any(Object),
- 0,
- undefined,
- );
+ expect(storeActions.toggleStageCollapsed).toHaveBeenCalledWith(expect.any(Object), 0);
});
it('calls fetchJobs when stage is mounted', () => {
diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js
index b1da89d7a9b..20adaa7abbc 100644
--- a/spec/frontend/ide/components/merge_requests/item_spec.js
+++ b/spec/frontend/ide/components/merge_requests/item_spec.js
@@ -33,7 +33,7 @@ describe('IDE merge request item', () => {
store,
});
};
- const findIcon = () => wrapper.find('.ic-mobile-issue-close');
+ const findIcon = () => wrapper.find('[data-testid="mobile-issue-close-icon"]');
beforeEach(() => {
store = createStore();
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
index e2c6ac49e07..80dcd861451 100644
--- a/spec/frontend/ide/components/merge_requests/list_spec.js
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -56,14 +56,10 @@ describe('IDE merge requests list', () => {
it('calls fetch on mounted', () => {
createComponent();
- expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
- expect.any(Object),
- {
- search: '',
- type: '',
- },
- undefined,
- );
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
+ search: '',
+ type: '',
+ });
});
it('renders loading icon when merge request is loading', () => {
@@ -95,14 +91,10 @@ describe('IDE merge requests list', () => {
const searchType = wrapper.vm.$options.searchTypes[0];
expect(findTokenedInput().props('tokens')).toEqual([searchType]);
- expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
- expect.any(Object),
- {
- type: searchType.type,
- search: '',
- },
- undefined,
- );
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
+ type: searchType.type,
+ search: '',
+ });
});
});
@@ -136,14 +128,10 @@ describe('IDE merge requests list', () => {
input.vm.$emit('input', 'something');
return wrapper.vm.$nextTick().then(() => {
- expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
- expect.any(Object),
- {
- search: 'something',
- type: '',
- },
- undefined,
- );
+ expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
+ search: 'something',
+ type: '',
+ });
});
});
});
diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js
index 2aa3992a6d8..c98aa313f40 100644
--- a/spec/frontend/ide/components/nav_dropdown_button_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js
@@ -23,7 +23,7 @@ describe('NavDropdown', () => {
vm.$mount();
};
- const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
+ const findIcon = name => vm.$el.querySelector(`[data-testid="${name}-icon"]`);
const findMRIcon = () => findIcon('merge-request');
const findBranchIcon = () => findIcon('branch');
diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js
index ce123d925c8..2f91ab7af0a 100644
--- a/spec/frontend/ide/components/nav_dropdown_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_spec.js
@@ -39,7 +39,7 @@ describe('IDE NavDropdown', () => {
});
};
- const findIcon = name => wrapper.find(`.ic-${name}`);
+ const findIcon = name => wrapper.find(`[data-testid="${name}-icon"]`);
const findMRIcon = () => findIcon('merge-request');
const findNavForm = () => wrapper.find('.ide-nav-form');
const showDropdown = () => {
diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js
index 3c611b7de8f..66317296ee9 100644
--- a/spec/frontend/ide/components/new_dropdown/button_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/button_spec.js
@@ -28,7 +28,7 @@ describe('IDE new entry dropdown button component', () => {
});
it('renders icon', () => {
- expect(vm.$el.querySelector('.ic-doc-new')).not.toBe(null);
+ expect(vm.$el.querySelector('[data-testid="doc-new-icon"]')).not.toBe(null);
});
it('emits click event', () => {
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index ad27954cd10..ae497106f73 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -85,7 +85,6 @@ describe('new dropdown upload', () => {
name: textFile.name,
type: 'blob',
content: 'plain text',
- binary: false,
rawPath: '',
});
})
@@ -102,7 +101,6 @@ describe('new dropdown upload', () => {
name: binaryFile.name,
type: 'blob',
content: binaryTarget.result.split('base64,')[1],
- binary: true,
rawPath: binaryTarget.result,
});
});
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index 86cdbafaff9..7f083fa7c25 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -22,11 +22,11 @@ describe('IDE pipelines list', () => {
const defaultState = {
links: { ciHelpPagePath: TEST_HOST },
pipelinesEmptyStateSvgPath: TEST_HOST,
- pipelines: {
- stages: [],
- failedStages: [],
- isLoadingJobs: false,
- },
+ };
+ const defaultPipelinesState = {
+ stages: [],
+ failedStages: [],
+ isLoadingJobs: false,
};
const fetchLatestPipelineMock = jest.fn();
@@ -34,23 +34,20 @@ describe('IDE pipelines list', () => {
const failedStagesGetterMock = jest.fn().mockReturnValue([]);
const fakeProjectPath = 'alpha/beta';
- const createComponent = (state = {}) => {
- const { pipelines: pipelinesState, ...restOfState } = state;
- const { defaultPipelines, ...defaultRestOfState } = defaultState;
-
- const fakeStore = new Vuex.Store({
+ const createStore = (rootState, pipelinesState) => {
+ return new Vuex.Store({
getters: {
currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }),
},
state: {
- ...defaultRestOfState,
- ...restOfState,
+ ...defaultState,
+ ...rootState,
},
modules: {
pipelines: {
namespaced: true,
state: {
- ...defaultPipelines,
+ ...defaultPipelinesState,
...pipelinesState,
},
actions: {
@@ -69,10 +66,12 @@ describe('IDE pipelines list', () => {
},
},
});
+ };
+ const createComponent = (state = {}, pipelinesState = {}) => {
wrapper = shallowMount(List, {
localVue,
- store: fakeStore,
+ store: createStore(state, pipelinesState),
});
};
@@ -94,31 +93,33 @@ describe('IDE pipelines list', () => {
describe('when loading', () => {
let defaultPipelinesLoadingState;
+
beforeAll(() => {
defaultPipelinesLoadingState = {
- ...defaultState.pipelines,
isLoadingPipeline: true,
};
});
it('does not render when pipeline has loaded before', () => {
- createComponent({
- pipelines: {
+ createComponent(
+ {},
+ {
...defaultPipelinesLoadingState,
hasLoadedPipeline: true,
},
- });
+ );
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('renders loading state', () => {
- createComponent({
- pipelines: {
+ createComponent(
+ {},
+ {
...defaultPipelinesLoadingState,
hasLoadedPipeline: false,
},
- });
+ );
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
@@ -126,21 +127,22 @@ describe('IDE pipelines list', () => {
describe('when loaded', () => {
let defaultPipelinesLoadedState;
+
beforeAll(() => {
defaultPipelinesLoadedState = {
- ...defaultState.pipelines,
isLoadingPipeline: false,
hasLoadedPipeline: true,
};
});
it('renders empty state when no latestPipeline', () => {
- createComponent({ pipelines: { ...defaultPipelinesLoadedState, latestPipeline: null } });
+ createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null });
expect(wrapper.element).toMatchSnapshot();
});
describe('with latest pipeline loaded', () => {
let withLatestPipelineState;
+
beforeAll(() => {
withLatestPipelineState = {
...defaultPipelinesLoadedState,
@@ -149,12 +151,12 @@ describe('IDE pipelines list', () => {
});
it('renders ci icon', () => {
- createComponent({ pipelines: withLatestPipelineState });
+ createComponent({}, withLatestPipelineState);
expect(wrapper.find(CiIcon).exists()).toBe(true);
});
it('renders pipeline data', () => {
- createComponent({ pipelines: withLatestPipelineState });
+ createComponent({}, withLatestPipelineState);
expect(wrapper.text()).toContain('#1');
});
@@ -162,7 +164,7 @@ describe('IDE pipelines list', () => {
it('renders list of jobs', () => {
const stages = [];
const isLoadingJobs = true;
- createComponent({ pipelines: { ...withLatestPipelineState, stages, isLoadingJobs } });
+ createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs });
const jobProps = wrapper
.findAll(Tab)
@@ -177,7 +179,7 @@ describe('IDE pipelines list', () => {
const failedStages = [];
failedStagesGetterMock.mockReset().mockReturnValue(failedStages);
const isLoadingJobs = true;
- createComponent({ pipelines: { ...withLatestPipelineState, isLoadingJobs } });
+ createComponent({}, { ...withLatestPipelineState, isLoadingJobs });
const jobProps = wrapper
.findAll(Tab)
@@ -191,12 +193,13 @@ describe('IDE pipelines list', () => {
describe('with YAML error', () => {
it('renders YAML error', () => {
const yamlError = 'test yaml error';
- createComponent({
- pipelines: {
+ createComponent(
+ {},
+ {
...defaultPipelinesLoadedState,
latestPipeline: { ...pipelines[0], yamlError },
},
- });
+ );
expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
expect(wrapper.text()).toContain(yamlError);
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 7b2025f5e9f..7b22f75cee4 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -279,24 +279,16 @@ describe('IDE clientside preview', () => {
});
it('calls getFileData', () => {
- expect(storeActions.getFileData).toHaveBeenCalledWith(
- expect.any(Object),
- {
- path: 'package.json',
- makeFileActive: false,
- },
- undefined, // vuex callback
- );
+ expect(storeActions.getFileData).toHaveBeenCalledWith(expect.any(Object), {
+ path: 'package.json',
+ makeFileActive: false,
+ });
});
it('calls getRawFileData', () => {
- expect(storeActions.getRawFileData).toHaveBeenCalledWith(
- expect.any(Object),
- {
- path: 'package.json',
- },
- undefined, // vuex callback
- );
+ expect(storeActions.getRawFileData).toHaveBeenCalledWith(expect.any(Object), {
+ path: 'package.json',
+ });
});
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index f0ae2ba732b..9f4c9c1622a 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -45,7 +45,7 @@ describe('RepoEditor', () => {
const createOpenFile = path => {
const origFile = store.state.openFiles[0];
- const newFile = { ...origFile, path, key: path };
+ const newFile = { ...origFile, path, key: path, name: 'myfile.txt', content: 'hello world' };
store.state.entries[path] = newFile;
@@ -54,8 +54,9 @@ describe('RepoEditor', () => {
beforeEach(() => {
const f = {
- ...file(),
+ ...file('file.txt'),
viewMode: FILE_VIEW_MODE_EDITOR,
+ content: 'hello world',
};
const storeOptions = createStoreOptions();
@@ -142,6 +143,7 @@ describe('RepoEditor', () => {
...vm.file,
projectId: 'namespace/project',
path: 'sample.md',
+ name: 'sample.md',
content: 'testing 123',
});
@@ -200,7 +202,8 @@ describe('RepoEditor', () => {
describe('when open file is binary and not raw', () => {
beforeEach(done => {
- vm.file.binary = true;
+ vm.file.name = 'file.dat';
+ vm.file.content = '🐱'; // non-ascii binary content
vm.$nextTick(done);
});
@@ -407,6 +410,9 @@ describe('RepoEditor', () => {
beforeEach(done => {
jest.spyOn(vm.editor, 'updateDimensions').mockImplementation();
vm.file.viewMode = FILE_VIEW_MODE_PREVIEW;
+ vm.file.name = 'myfile.md';
+ vm.file.content = 'hello world';
+
vm.$nextTick(done);
});
@@ -650,7 +656,6 @@ describe('RepoEditor', () => {
path: 'foo/foo.png',
type: 'blob',
content: 'Zm9v',
- binary: true,
rawPath: 'data:image/png;base64,Zm9v',
});
});
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index 5a591d3dcd0..f35726de27c 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -1,21 +1,24 @@
-import Vue from 'vue';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { createStore } from '~/ide/stores';
-import repoTab from '~/ide/components/repo_tab.vue';
+import RepoTab from '~/ide/components/repo_tab.vue';
import { createRouter } from '~/ide/ide_router';
import { file } from '../helpers';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('RepoTab', () => {
- let vm;
+ let wrapper;
let store;
let router;
function createComponent(propsData) {
- const RepoTab = Vue.extend(repoTab);
-
- return new RepoTab({
+ wrapper = mount(RepoTab, {
+ localVue,
store,
propsData,
- }).$mount();
+ });
}
beforeEach(() => {
@@ -25,23 +28,24 @@ describe('RepoTab', () => {
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
it('renders a close link and a name link', () => {
- vm = createComponent({
+ createComponent({
tab: file(),
});
- vm.$store.state.openFiles.push(vm.tab);
- const close = vm.$el.querySelector('.multi-file-tab-close');
- const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
+ wrapper.vm.$store.state.openFiles.push(wrapper.vm.tab);
+ const close = wrapper.find('.multi-file-tab-close');
+ const name = wrapper.find(`[title]`);
- expect(close.innerHTML).toContain('#close');
- expect(name.textContent.trim()).toEqual(vm.tab.name);
+ expect(close.html()).toContain('#close');
+ expect(name.text().trim()).toEqual(wrapper.vm.tab.name);
});
- it('does not call openPendingTab when tab is active', done => {
- vm = createComponent({
+ it('does not call openPendingTab when tab is active', async () => {
+ createComponent({
tab: {
...file(),
pending: true,
@@ -49,63 +53,51 @@ describe('RepoTab', () => {
},
});
- jest.spyOn(vm, 'openPendingTab').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'openPendingTab').mockImplementation(() => {});
- vm.$el.click();
+ await wrapper.trigger('click');
- vm.$nextTick(() => {
- expect(vm.openPendingTab).not.toHaveBeenCalled();
-
- done();
- });
+ expect(wrapper.vm.openPendingTab).not.toHaveBeenCalled();
});
it('fires clickFile when the link is clicked', () => {
- vm = createComponent({
+ createComponent({
tab: file(),
});
- jest.spyOn(vm, 'clickFile').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'clickFile').mockImplementation(() => {});
- vm.$el.click();
+ wrapper.trigger('click');
- expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
+ expect(wrapper.vm.clickFile).toHaveBeenCalledWith(wrapper.vm.tab);
});
it('calls closeFile when clicking close button', () => {
- vm = createComponent({
+ createComponent({
tab: file(),
});
- jest.spyOn(vm, 'closeFile').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'closeFile').mockImplementation(() => {});
- vm.$el.querySelector('.multi-file-tab-close').click();
+ wrapper.find('.multi-file-tab-close').trigger('click');
- expect(vm.closeFile).toHaveBeenCalledWith(vm.tab);
+ expect(wrapper.vm.closeFile).toHaveBeenCalledWith(wrapper.vm.tab);
});
- it('changes icon on hover', done => {
+ it('changes icon on hover', async () => {
const tab = file();
tab.changed = true;
- vm = createComponent({
+ createComponent({
tab,
});
- vm.$el.dispatchEvent(new Event('mouseover'));
+ await wrapper.trigger('mouseover');
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.file-modified')).toBeNull();
+ expect(wrapper.find('.file-modified').exists()).toBe(false);
- vm.$el.dispatchEvent(new Event('mouseout'));
- })
- .then(Vue.nextTick)
- .then(() => {
- expect(vm.$el.querySelector('.file-modified')).not.toBeNull();
+ await wrapper.trigger('mouseout');
- done();
- })
- .catch(done.fail);
+ expect(wrapper.find('.file-modified').exists()).toBe(true);
});
describe('locked file', () => {
@@ -120,21 +112,17 @@ describe('RepoTab', () => {
},
};
- vm = createComponent({
+ createComponent({
tab: f,
});
});
- afterEach(() => {
- vm.$destroy();
- });
-
it('renders lock icon', () => {
- expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ expect(wrapper.find('.file-status-icon')).not.toBeNull();
});
it('renders a tooltip', () => {
- expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain(
+ expect(wrapper.find('span:nth-child(2)').attributes('data-original-title')).toContain(
'Locked by testuser',
);
});
@@ -142,45 +130,37 @@ describe('RepoTab', () => {
describe('methods', () => {
describe('closeTab', () => {
- it('closes tab if file has changed', done => {
+ it('closes tab if file has changed', async () => {
const tab = file();
tab.changed = true;
tab.opened = true;
- vm = createComponent({
+ createComponent({
tab,
});
- vm.$store.state.openFiles.push(tab);
- vm.$store.state.changedFiles.push(tab);
- vm.$store.state.entries[tab.path] = tab;
- vm.$store.dispatch('setFileActive', tab.path);
-
- vm.$el.querySelector('.multi-file-tab-close').click();
+ wrapper.vm.$store.state.openFiles.push(tab);
+ wrapper.vm.$store.state.changedFiles.push(tab);
+ wrapper.vm.$store.state.entries[tab.path] = tab;
+ wrapper.vm.$store.dispatch('setFileActive', tab.path);
- vm.$nextTick(() => {
- expect(tab.opened).toBeFalsy();
- expect(vm.$store.state.changedFiles.length).toBe(1);
+ await wrapper.find('.multi-file-tab-close').trigger('click');
- done();
- });
+ expect(tab.opened).toBeFalsy();
+ expect(wrapper.vm.$store.state.changedFiles).toHaveLength(1);
});
- it('closes tab when clicking close btn', done => {
+ it('closes tab when clicking close btn', async () => {
const tab = file('lose');
tab.opened = true;
- vm = createComponent({
+ createComponent({
tab,
});
- vm.$store.state.openFiles.push(tab);
- vm.$store.state.entries[tab.path] = tab;
- vm.$store.dispatch('setFileActive', tab.path);
+ wrapper.vm.$store.state.openFiles.push(tab);
+ wrapper.vm.$store.state.entries[tab.path] = tab;
+ wrapper.vm.$store.dispatch('setFileActive', tab.path);
- vm.$el.querySelector('.multi-file-tab-close').click();
+ await wrapper.find('.multi-file-tab-close').trigger('click');
- vm.$nextTick(() => {
- expect(tab.opened).toBeFalsy();
-
- done();
- });
+ expect(tab.opened).toBeFalsy();
});
});
});
diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js
index df5b01770f5..b251f207853 100644
--- a/spec/frontend/ide/components/repo_tabs_spec.js
+++ b/spec/frontend/ide/components/repo_tabs_spec.js
@@ -1,27 +1,40 @@
-import Vue from 'vue';
-import repoTabs from '~/ide/components/repo_tabs.vue';
-import createComponent from '../../helpers/vue_mount_component_helper';
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { createStore } from '~/ide/stores';
+import RepoTabs from '~/ide/components/repo_tabs.vue';
import { file } from '../helpers';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('RepoTabs', () => {
- const openedFiles = [file('open1'), file('open2')];
- const RepoTabs = Vue.extend(repoTabs);
- let vm;
+ let wrapper;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.openFiles = [file('open1'), file('open2')];
+
+ wrapper = mount(RepoTabs, {
+ propsData: {
+ files: store.state.openFiles,
+ viewer: 'editor',
+ activeFile: file('activeFile'),
+ },
+ store,
+ localVue,
+ });
+ });
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders a list of tabs', done => {
- vm = createComponent(RepoTabs, {
- files: openedFiles,
- viewer: 'editor',
- activeFile: file('activeFile'),
- });
- openedFiles[0].active = true;
+ store.state.openFiles[0].active = true;
- vm.$nextTick(() => {
- const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
+ wrapper.vm.$nextTick(() => {
+ const tabs = [...wrapper.vm.$el.querySelectorAll('.multi-file-tab')];
expect(tabs.length).toEqual(2);
expect(tabs[0].parentNode.classList.contains('active')).toEqual(true);
diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js
index 2399446ed15..ce61a31691a 100644
--- a/spec/frontend/ide/components/terminal/session_spec.js
+++ b/spec/frontend/ide/components/terminal/session_spec.js
@@ -52,7 +52,7 @@ describe('IDE TerminalSession', () => {
state.session = null;
factory();
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
});
it('shows terminal', () => {
diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
index 6c2871abb46..c22063e1d72 100644
--- a/spec/frontend/ide/components/terminal/terminal_controls_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
@@ -42,24 +42,24 @@ describe('IDE TerminalControls', () => {
it('emits "scroll-up" when click up button', () => {
factory({ propsData: { canScrollUp: true } });
- expect(wrapper.emittedByOrder()).toEqual([]);
+ expect(wrapper.emitted()).toEqual({});
buttons.at(0).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emittedByOrder()).toEqual([{ name: 'scroll-up', args: [] }]);
+ expect(wrapper.emitted('scroll-up')).toEqual([[]]);
});
});
it('emits "scroll-down" when click down button', () => {
factory({ propsData: { canScrollDown: true } });
- expect(wrapper.emittedByOrder()).toEqual([]);
+ expect(wrapper.emitted()).toEqual({});
buttons.at(1).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emittedByOrder()).toEqual([{ name: 'scroll-down', args: [] }]);
+ expect(wrapper.emitted('scroll-down')).toEqual([[]]);
});
});
});
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 16a76fae1dd..9adf5848f9d 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
@@ -1,13 +1,12 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue';
import {
MSG_TERMINAL_SYNC_CONNECTING,
MSG_TERMINAL_SYNC_UPLOADING,
MSG_TERMINAL_SYNC_RUNNING,
} from '~/ide/stores/modules/terminal_sync/messages';
-import Icon from '~/vue_shared/components/icon.vue';
const TEST_MESSAGE = 'lorem ipsum dolar sit';
const START_LOADING = 'START_LOADING';
@@ -58,7 +57,7 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => {
it('shows nothing', () => {
createComponent();
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
});
});
@@ -80,7 +79,7 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => {
if (!icon) {
it('does not render icon', () => {
- expect(wrapper.find(Icon).exists()).toBe(false);
+ expect(wrapper.find(GlIcon).exists()).toBe(false);
});
it('renders loading icon', () => {
@@ -88,7 +87,7 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => {
});
} else {
it('renders icon', () => {
- expect(wrapper.find(Icon).props('name')).toEqual(icon);
+ expect(wrapper.find(GlIcon).props('name')).toEqual(icon);
});
it('does not render loading icon', () => {
diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js
index 8caa9c2b437..0e85b523cbd 100644
--- a/spec/frontend/ide/helpers.js
+++ b/spec/frontend/ide/helpers.js
@@ -6,7 +6,6 @@ export const file = (name = 'name', id = name, type = '', parent = null) =>
id,
type,
icon: 'icon',
- url: 'url',
name,
path: parent ? `${parent.path}/${name}` : name,
parentPath: parent ? parent.path : '',
diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js
index 529f80e6f6f..01c2eab33a5 100644
--- a/spec/frontend/ide/lib/editor_spec.js
+++ b/spec/frontend/ide/lib/editor_spec.js
@@ -202,28 +202,6 @@ describe('Multi-file editor library', () => {
});
});
- describe('schemas', () => {
- let originalGon;
-
- beforeEach(() => {
- originalGon = window.gon;
- window.gon = { features: { schemaLinting: true } };
-
- delete Editor.editorInstance;
- instance = Editor.create();
- });
-
- afterEach(() => {
- window.gon = originalGon;
- });
-
- it('registers custom schemas defined with Monaco', () => {
- expect(monacoLanguages.yaml.yamlDefaults.diagnosticsOptions).toMatchObject({
- schemas: [{ fileMatch: ['*.gitlab-ci.yml'] }],
- });
- });
- });
-
describe('replaceSelectedText', () => {
let model;
let editor;
diff --git a/spec/frontend/ide/lib/errors_spec.js b/spec/frontend/ide/lib/errors_spec.js
new file mode 100644
index 00000000000..8c3fb378302
--- /dev/null
+++ b/spec/frontend/ide/lib/errors_spec.js
@@ -0,0 +1,70 @@
+import {
+ createUnexpectedCommitError,
+ createCodeownersCommitError,
+ createBranchChangedCommitError,
+ parseCommitError,
+} from '~/ide/lib/errors';
+
+const TEST_SPECIAL = '&special<';
+const TEST_SPECIAL_ESCAPED = '&amp;special&lt;';
+const TEST_MESSAGE = 'Test message.';
+const CODEOWNERS_MESSAGE =
+ 'Push to protected branches that contain changes to files matching CODEOWNERS is not allowed';
+const CHANGED_MESSAGE = 'Things changed since you started editing';
+
+describe('~/ide/lib/errors', () => {
+ const createResponseError = message => ({
+ response: {
+ data: {
+ message,
+ },
+ },
+ });
+
+ describe('createCodeownersCommitError', () => {
+ it('uses given message', () => {
+ expect(createCodeownersCommitError(TEST_MESSAGE)).toEqual({
+ title: 'CODEOWNERS rule violation',
+ messageHTML: TEST_MESSAGE,
+ canCreateBranch: true,
+ });
+ });
+
+ it('escapes special chars', () => {
+ expect(createCodeownersCommitError(TEST_SPECIAL)).toEqual({
+ title: 'CODEOWNERS rule violation',
+ messageHTML: TEST_SPECIAL_ESCAPED,
+ canCreateBranch: true,
+ });
+ });
+ });
+
+ describe('createBranchChangedCommitError', () => {
+ it.each`
+ message | expectedMessage
+ ${TEST_MESSAGE} | ${`${TEST_MESSAGE}<br/><br/>Would you like to create a new branch?`}
+ ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}<br/><br/>Would you like to create a new branch?`}
+ `('uses given message="$message"', ({ message, expectedMessage }) => {
+ expect(createBranchChangedCommitError(message)).toEqual({
+ title: 'Branch changed',
+ messageHTML: expectedMessage,
+ canCreateBranch: true,
+ });
+ });
+ });
+
+ describe('parseCommitError', () => {
+ it.each`
+ message | expectation
+ ${null} | ${createUnexpectedCommitError()}
+ ${{}} | ${createUnexpectedCommitError()}
+ ${{ response: {} }} | ${createUnexpectedCommitError()}
+ ${{ response: { data: {} } }} | ${createUnexpectedCommitError()}
+ ${createResponseError('test')} | ${createUnexpectedCommitError()}
+ ${createResponseError(CODEOWNERS_MESSAGE)} | ${createCodeownersCommitError(CODEOWNERS_MESSAGE)}
+ ${createResponseError(CHANGED_MESSAGE)} | ${createBranchChangedCommitError(CHANGED_MESSAGE)}
+ `('parses message into error object with "$message"', ({ message, expectation }) => {
+ expect(parseCommitError(message)).toEqual(expectation);
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js
index 6974cdc4074..8ca6f01d9a6 100644
--- a/spec/frontend/ide/lib/files_spec.js
+++ b/spec/frontend/ide/lib/files_spec.js
@@ -1,29 +1,16 @@
-import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateFiles, splitParent } from '~/ide/lib/files';
import { decorateData } from '~/ide/stores/utils';
-const TEST_BRANCH_ID = 'lorem-ipsum';
-const TEST_PROJECT_ID = 10;
-
const createEntries = paths => {
const createEntry = (acc, { path, type, children }) => {
- // Sometimes we need to end the url with a '/'
- const createUrl = base => (type === 'tree' ? `${base}/` : base);
-
const { name, parent } = splitParent(path);
- const previewMode = viewerInformationForPath(name);
acc[path] = {
...decorateData({
- projectId: TEST_PROJECT_ID,
- branchId: TEST_BRANCH_ID,
id: path,
name,
path,
- url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${path}`),
type,
- previewMode,
- binary: (previewMode && previewMode.binary) || false,
parentPath: parent,
}),
tree: children.map(childName => expect.objectContaining({ name: childName })),
@@ -56,11 +43,7 @@ describe('IDE lib decorate files', () => {
{ path: 'README.md', type: 'blob', children: [] },
]);
- const { entries, treeList } = decorateFiles({
- data,
- branchId: TEST_BRANCH_ID,
- projectId: TEST_PROJECT_ID,
- });
+ const { entries, treeList } = decorateFiles({ data });
// Here we test the keys and then each key/value individually because `expect(entries).toEqual(expectedEntries)`
// was taking a very long time for some reason. Probably due to large objects and nested `expect.objectContaining`.
diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js
index 472516b6a2c..c8925e6745d 100644
--- a/spec/frontend/ide/mock_data.js
+++ b/spec/frontend/ide/mock_data.js
@@ -112,7 +112,8 @@ export const jobs = [
{
id: 4,
name: 'test 4',
- path: 'testing4',
+ // bridge jobs don't have details page and so there is no path attribute
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/216480
status: {
icon: 'status_failed',
text: 'failed',
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index bc3f86702cf..d2c32a81811 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -146,7 +146,7 @@ describe('IDE services', () => {
it('gives back file.baseRaw for files with that property present', () => {
file.baseRaw = TEST_FILE_CONTENTS;
- return services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
+ return services.getBaseRawFileData(file, TEST_PROJECT_ID, TEST_COMMIT_SHA).then(content => {
expect(content).toEqual(TEST_FILE_CONTENTS);
});
});
@@ -155,7 +155,7 @@ describe('IDE services', () => {
file.tempFile = true;
file.baseRaw = TEST_FILE_CONTENTS;
- return services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
+ return services.getBaseRawFileData(file, TEST_PROJECT_ID, TEST_COMMIT_SHA).then(content => {
expect(content).toEqual(TEST_FILE_CONTENTS);
});
});
@@ -192,7 +192,7 @@ describe('IDE services', () => {
});
it('fetches file content', () =>
- services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
+ services.getBaseRawFileData(file, TEST_PROJECT_ID, TEST_COMMIT_SHA).then(content => {
expect(content).toEqual(TEST_FILE_CONTENTS);
}));
},
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 88e7a9fff36..974c0715c06 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -27,6 +27,10 @@ describe('IDE store file actions', () => {
};
store = createStore();
+
+ store.state.currentProjectId = 'test/test';
+ store.state.currentBranchId = 'master';
+
router = createRouter(store);
jest.spyOn(store, 'commit');
@@ -72,10 +76,7 @@ describe('IDE store file actions', () => {
});
it('closes file & opens next available file', () => {
- const f = {
- ...file('newOpenFile'),
- url: '/newOpenFile',
- };
+ const f = file('newOpenFile');
store.state.openFiles.push(f);
store.state.entries[f.path] = f;
@@ -84,7 +85,7 @@ describe('IDE store file actions', () => {
.dispatch('closeFile', localFile)
.then(Vue.nextTick)
.then(() => {
- expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
+ expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/master/-/newOpenFile/');
});
});
@@ -240,7 +241,6 @@ describe('IDE store file actions', () => {
200,
{
raw_path: 'raw_path',
- binary: false,
},
{
'page-title': 'testing getFileData',
@@ -296,7 +296,6 @@ describe('IDE store file actions', () => {
describe('Re-named success', () => {
beforeEach(() => {
localFile = file(`newCreate-${Math.random()}`);
- localFile.url = `project/getFileDataURL`;
localFile.prevPath = 'old-dull-file';
localFile.path = 'new-shiny-file';
store.state.entries[localFile.path] = localFile;
@@ -305,7 +304,6 @@ describe('IDE store file actions', () => {
200,
{
raw_path: 'raw_path',
- binary: false,
},
{
'page-title': 'testing old-dull-file',
@@ -393,7 +391,11 @@ describe('IDE store file actions', () => {
tmpFile.mrChange = { new_file: false };
return store.dispatch('getRawFileData', { path: tmpFile.path }).then(() => {
- expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
+ expect(service.getBaseRawFileData).toHaveBeenCalledWith(
+ tmpFile,
+ 'gitlab-org/gitlab-ce',
+ 'SHA',
+ );
expect(tmpFile.baseRaw).toBe('baseraw');
});
});
@@ -660,7 +662,7 @@ describe('IDE store file actions', () => {
});
it('pushes route for active file', () => {
- expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`);
+ expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/master/-/tempFile/');
});
});
});
@@ -735,10 +737,8 @@ describe('IDE store file actions', () => {
});
it('pushes router URL when added', () => {
- store.state.currentBranchId = 'master';
-
return store.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }).then(() => {
- expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/');
+ expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/master/');
});
});
});
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index 62971b9cad6..b1cceda9d85 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -453,11 +453,9 @@ describe('IDE store merge request actions', () => {
it('updates activity bar view and gets file data, if changes are found', done => {
store.state.entries.foo = {
- url: 'test',
type: 'blob',
};
store.state.entries.bar = {
- url: 'test',
type: 'blob',
};
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index f77dbd80025..ebf39df2f6f 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -123,7 +123,6 @@ describe('Multi-file store actions', () => {
it('creates temp tree', done => {
store
.dispatch('createTempEntry', {
- branchId: store.state.currentBranchId,
name: 'test',
type: 'tree',
})
@@ -150,7 +149,6 @@ describe('Multi-file store actions', () => {
store
.dispatch('createTempEntry', {
- branchId: store.state.currentBranchId,
name: 'testing/test',
type: 'tree',
})
@@ -176,7 +174,6 @@ describe('Multi-file store actions', () => {
store
.dispatch('createTempEntry', {
- branchId: store.state.currentBranchId,
name: 'testing',
type: 'tree',
})
@@ -197,7 +194,6 @@ describe('Multi-file store actions', () => {
store
.dispatch('createTempEntry', {
name,
- branchId: 'mybranch',
type: 'blob',
})
.then(() => {
@@ -217,7 +213,6 @@ describe('Multi-file store actions', () => {
store
.dispatch('createTempEntry', {
name,
- branchId: 'mybranch',
type: 'blob',
})
.then(() => {
@@ -237,7 +232,6 @@ describe('Multi-file store actions', () => {
store
.dispatch('createTempEntry', {
name,
- branchId: 'mybranch',
type: 'blob',
})
.then(() => {
@@ -249,7 +243,7 @@ describe('Multi-file store actions', () => {
});
it('sets tmp file as active', () => {
- createTempEntry(store, { name: 'test', branchId: 'mybranch', type: 'blob' });
+ createTempEntry(store, { name: 'test', type: 'blob' });
expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
});
@@ -262,7 +256,6 @@ describe('Multi-file store actions', () => {
store
.dispatch('createTempEntry', {
name: 'test',
- branchId: 'mybranch',
type: 'blob',
})
.then(() => {
@@ -780,9 +773,11 @@ describe('Multi-file store actions', () => {
});
it('routes to the renamed file if the original file has been opened', done => {
+ store.state.currentProjectId = 'test/test';
+ store.state.currentBranchId = 'master';
+
Object.assign(store.state.entries.orig, {
opened: true,
- url: '/foo-bar.md',
});
store
@@ -792,7 +787,7 @@ describe('Multi-file store actions', () => {
})
.then(() => {
expect(router.push.mock.calls).toHaveLength(1);
- expect(router.push).toHaveBeenCalledWith(`/project/foo-bar.md`);
+ expect(router.push).toHaveBeenCalledWith(`/project/test/test/tree/master/-/renamed/`);
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index dcf05329ce0..e24f08fa802 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -1,3 +1,4 @@
+import { TEST_HOST } from 'helpers/test_constants';
import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
@@ -482,4 +483,48 @@ describe('IDE store getters', () => {
expect(localStore.getters.getAvailableFileName('foo-bar1.jpg')).toBe('foo-bar1.jpg');
});
});
+
+ describe('getUrlForPath', () => {
+ it('returns a route url for the given path', () => {
+ localState.currentProjectId = 'test/test';
+ localState.currentBranchId = 'master';
+
+ expect(localStore.getters.getUrlForPath('path/to/foo/bar-1.jpg')).toBe(
+ `/project/test/test/tree/master/-/path/to/foo/bar-1.jpg/`,
+ );
+ });
+ });
+
+ describe('getJsonSchemaForPath', () => {
+ beforeEach(() => {
+ localState.currentProjectId = 'path/to/some/project';
+ localState.currentBranchId = 'master';
+ });
+
+ it('returns a json schema uri and match config for a json/yaml file that can be loaded by monaco', () => {
+ expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
+ fileMatch: ['*.gitlab-ci.yml'],
+ uri: `${TEST_HOST}/path/to/some/project/-/schema/master/.gitlab-ci.yml`,
+ });
+ });
+
+ it('returns a path containing sha if branch details are present in state', () => {
+ localState.projects['path/to/some/project'] = {
+ name: 'project',
+ branches: {
+ master: {
+ name: 'master',
+ commit: {
+ id: 'abcdef123456',
+ },
+ },
+ },
+ };
+
+ expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
+ fileMatch: ['*.gitlab-ci.yml'],
+ uri: `${TEST_HOST}/path/to/some/project/-/schema/abcdef123456/.gitlab-ci.yml`,
+ });
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/integration_spec.js b/spec/frontend/ide/stores/integration_spec.js
index f95f036f572..b6a7c7fd02d 100644
--- a/spec/frontend/ide/stores/integration_spec.js
+++ b/spec/frontend/ide/stores/integration_spec.js
@@ -36,8 +36,6 @@ describe('IDE store integration', () => {
beforeEach(() => {
const { entries, treeList } = decorateFiles({
data: [`${TEST_PATH_DIR}/`, TEST_PATH, 'README.md'],
- projectId: TEST_PROJECT_ID,
- branchId: TEST_BRANCH,
});
Object.assign(entries[TEST_PATH], {
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index a14879112fd..babc50e54f1 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -9,6 +9,7 @@ import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
import * as actions from '~/ide/stores/modules/commit/actions';
+import { createUnexpectedCommitError } from '~/ide/lib/errors';
import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import testAction from '../../../../helpers/vuex_action_helper';
@@ -510,7 +511,7 @@ describe('IDE commit module actions', () => {
});
});
- describe('failed', () => {
+ describe('success response with failed message', () => {
beforeEach(() => {
jest.spyOn(service, 'commit').mockResolvedValue({
data: {
@@ -533,6 +534,25 @@ describe('IDE commit module actions', () => {
});
});
+ describe('failed response', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'commit').mockRejectedValue({});
+ });
+
+ it('commits error updates', async () => {
+ jest.spyOn(store, 'commit');
+
+ await store.dispatch('commit/commitChanges').catch(() => {});
+
+ expect(store.commit.mock.calls).toEqual([
+ ['commit/CLEAR_ERROR', undefined, undefined],
+ ['commit/UPDATE_LOADING', true, undefined],
+ ['commit/UPDATE_LOADING', false, undefined],
+ ['commit/SET_ERROR', createUnexpectedCommitError(), undefined],
+ ]);
+ });
+ });
+
describe('first commit of a branch', () => {
const COMMIT_RESPONSE = {
id: '123456',
diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
index 45ac1a86ab3..6393a70eac6 100644
--- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
@@ -1,5 +1,6 @@
import commitState from '~/ide/stores/modules/commit/state';
import mutations from '~/ide/stores/modules/commit/mutations';
+import * as types from '~/ide/stores/modules/commit/mutation_types';
describe('IDE commit module mutations', () => {
let state;
@@ -62,4 +63,24 @@ describe('IDE commit module mutations', () => {
expect(state.shouldCreateMR).toBe(false);
});
});
+
+ describe(types.CLEAR_ERROR, () => {
+ it('should clear commitError', () => {
+ state.commitError = {};
+
+ mutations[types.CLEAR_ERROR](state);
+
+ expect(state.commitError).toBeNull();
+ });
+ });
+
+ describe(types.SET_ERROR, () => {
+ it('should set commitError', () => {
+ const error = { title: 'foo' };
+
+ mutations[types.SET_ERROR](state, error);
+
+ expect(state.commitError).toBe(error);
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index 71918e7e2c2..8511843cc92 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -15,10 +15,10 @@ import {
fetchJobs,
toggleStageCollapsed,
setDetailJob,
- requestJobTrace,
- receiveJobTraceError,
- receiveJobTraceSuccess,
- fetchJobTrace,
+ requestJobLogs,
+ receiveJobLogsError,
+ receiveJobLogsSuccess,
+ fetchJobLogs,
resetLatestPipeline,
} from '~/ide/stores/modules/pipelines/actions';
import state from '~/ide/stores/modules/pipelines/state';
@@ -324,24 +324,24 @@ describe('IDE pipelines actions', () => {
});
});
- describe('requestJobTrace', () => {
+ describe('requestJobLogs', () => {
it('commits request', done => {
- testAction(requestJobTrace, null, mockedState, [{ type: types.REQUEST_JOB_TRACE }], [], done);
+ testAction(requestJobLogs, null, mockedState, [{ type: types.REQUEST_JOB_LOGS }], [], done);
});
});
- describe('receiveJobTraceError', () => {
+ describe('receiveJobLogsError', () => {
it('commits error', done => {
testAction(
- receiveJobTraceError,
+ receiveJobLogsError,
null,
mockedState,
- [{ type: types.RECEIVE_JOB_TRACE_ERROR }],
+ [{ type: types.RECEIVE_JOB_LOGS_ERROR }],
[
{
type: 'setErrorMessage',
payload: {
- text: 'An error occurred while fetching the job trace.',
+ text: 'An error occurred while fetching the job logs.',
action: expect.any(Function),
actionText: 'Please try again',
actionPayload: null,
@@ -353,20 +353,20 @@ describe('IDE pipelines actions', () => {
});
});
- describe('receiveJobTraceSuccess', () => {
+ describe('receiveJobLogsSuccess', () => {
it('commits data', done => {
testAction(
- receiveJobTraceSuccess,
+ receiveJobLogsSuccess,
'data',
mockedState,
- [{ type: types.RECEIVE_JOB_TRACE_SUCCESS, payload: 'data' }],
+ [{ type: types.RECEIVE_JOB_LOGS_SUCCESS, payload: 'data' }],
[],
done,
);
});
});
- describe('fetchJobTrace', () => {
+ describe('fetchJobLogs', () => {
beforeEach(() => {
mockedState.detailJob = { path: `${TEST_HOST}/project/builds` };
});
@@ -379,20 +379,20 @@ describe('IDE pipelines actions', () => {
it('dispatches request', done => {
testAction(
- fetchJobTrace,
+ fetchJobLogs,
null,
mockedState,
[],
[
- { type: 'requestJobTrace' },
- { type: 'receiveJobTraceSuccess', payload: { html: 'html' } },
+ { type: 'requestJobLogs' },
+ { type: 'receiveJobLogsSuccess', payload: { html: 'html' } },
],
done,
);
});
it('sends get request to correct URL', () => {
- fetchJobTrace({
+ fetchJobLogs({
state: mockedState,
dispatch() {},
@@ -410,11 +410,11 @@ describe('IDE pipelines actions', () => {
it('dispatches error', done => {
testAction(
- fetchJobTrace,
+ fetchJobLogs,
null,
mockedState,
[],
- [{ type: 'requestJobTrace' }, { type: 'receiveJobTraceError' }],
+ [{ type: 'requestJobLogs' }, { type: 'receiveJobLogsError' }],
done,
);
});
diff --git a/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js b/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js
index 3b7f92cfa74..7d2f5d5d710 100644
--- a/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js
@@ -175,37 +175,37 @@ describe('IDE pipelines mutations', () => {
});
});
- describe('REQUEST_JOB_TRACE', () => {
+ describe('REQUEST_JOB_LOGS', () => {
beforeEach(() => {
mockedState.detailJob = { ...jobs[0] };
});
it('sets loading on detail job', () => {
- mutations[types.REQUEST_JOB_TRACE](mockedState);
+ mutations[types.REQUEST_JOB_LOGS](mockedState);
expect(mockedState.detailJob.isLoading).toBe(true);
});
});
- describe('RECEIVE_JOB_TRACE_ERROR', () => {
+ describe('RECEIVE_JOB_LOGS_ERROR', () => {
beforeEach(() => {
mockedState.detailJob = { ...jobs[0], isLoading: true };
});
it('sets loading to false on detail job', () => {
- mutations[types.RECEIVE_JOB_TRACE_ERROR](mockedState);
+ mutations[types.RECEIVE_JOB_LOGS_ERROR](mockedState);
expect(mockedState.detailJob.isLoading).toBe(false);
});
});
- describe('RECEIVE_JOB_TRACE_SUCCESS', () => {
+ describe('RECEIVE_JOB_LOGS_SUCCESS', () => {
beforeEach(() => {
mockedState.detailJob = { ...jobs[0], isLoading: true };
});
it('sets output on detail job', () => {
- mutations[types.RECEIVE_JOB_TRACE_SUCCESS](mockedState, { html: 'html' });
+ mutations[types.RECEIVE_JOB_LOGS_SUCCESS](mockedState, { html: 'html' });
expect(mockedState.detailJob.output).toBe('html');
expect(mockedState.detailJob.isLoading).toBe(false);
});
diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js
index ff904bbc9cd..b53e40be980 100644
--- a/spec/frontend/ide/stores/mutations/file_spec.js
+++ b/spec/frontend/ide/stores/mutations/file_spec.js
@@ -61,13 +61,11 @@ describe('IDE store file mutations', () => {
mutations.SET_FILE_DATA(localState, {
data: {
raw_path: 'raw',
- binary: true,
},
file: localFile,
});
expect(localFile.rawPath).toBe('raw');
- expect(localFile.binary).toBeTruthy();
expect(localFile.raw).toBeNull();
expect(localFile.baseRaw).toBeNull();
});
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 1b29648fb8b..09e9481e5d4 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -113,8 +113,6 @@ describe('Multi-file store mutations', () => {
},
treeList: [tmpFile],
},
- projectId: 'gitlab-ce',
- branchId: 'master',
});
expect(localState.trees['gitlab-ce/master'].tree.length).toEqual(1);
@@ -272,7 +270,6 @@ describe('Multi-file store mutations', () => {
prevId: undefined,
prevPath: undefined,
prevName: undefined,
- prevUrl: undefined,
prevKey: undefined,
}),
);
@@ -337,7 +334,6 @@ describe('Multi-file store mutations', () => {
};
Object.assign(localState.entries['root-folder/oldPath'], {
parentPath: 'root-folder',
- url: 'root-folder/oldPath-blob-root-folder/oldPath',
});
mutations.RENAME_ENTRY(localState, {
@@ -366,9 +362,6 @@ describe('Multi-file store mutations', () => {
});
it('renames entry, preserving old parameters', () => {
- Object.assign(localState.entries.oldPath, {
- url: `project/-/oldPath`,
- });
const oldPathData = localState.entries.oldPath;
mutations.RENAME_ENTRY(localState, {
@@ -382,12 +375,10 @@ describe('Multi-file store mutations', () => {
id: 'newPath',
path: 'newPath',
name: 'newPath',
- url: `project/-/newPath`,
key: expect.stringMatching('newPath'),
prevId: 'oldPath',
prevName: 'oldPath',
prevPath: 'oldPath',
- prevUrl: `project/-/oldPath`,
prevKey: oldPathData.key,
prevParentPath: oldPathData.parentPath,
});
@@ -409,7 +400,6 @@ describe('Multi-file store mutations', () => {
prevId: expect.anything(),
prevName: expect.anything(),
prevPath: expect.anything(),
- prevUrl: expect.anything(),
prevKey: expect.anything(),
prevParentPath: expect.anything(),
}),
@@ -419,7 +409,7 @@ describe('Multi-file store mutations', () => {
it('properly handles files with spaces in name', () => {
const path = 'my fancy path';
const newPath = 'new path';
- const oldEntry = { ...file(path, path, 'blob'), url: `project/-/${path}` };
+ const oldEntry = file(path, path, 'blob');
localState.entries[path] = oldEntry;
@@ -435,12 +425,10 @@ describe('Multi-file store mutations', () => {
id: newPath,
path: newPath,
name: newPath,
- url: `project/-/new path`,
key: expect.stringMatching(newPath),
prevId: path,
prevName: path,
prevPath: path,
- prevUrl: `project/-/my fancy path`,
prevKey: oldEntry.key,
prevParentPath: oldEntry.parentPath,
});
@@ -549,7 +537,7 @@ describe('Multi-file store mutations', () => {
it('correctly saves original values if an entry is renamed multiple times', () => {
const original = { ...localState.entries.oldPath };
- const paramsToCheck = ['prevId', 'prevPath', 'prevName', 'prevUrl'];
+ const paramsToCheck = ['prevId', 'prevPath', 'prevName'];
const expectedObj = paramsToCheck.reduce(
(o, param) => ({ ...o, [param]: original[param.replace('prev', '').toLowerCase()] }),
{},
@@ -577,7 +565,6 @@ describe('Multi-file store mutations', () => {
prevId: 'lorem/orig',
prevPath: 'lorem/orig',
prevName: 'orig',
- prevUrl: 'project/-/loren/orig',
prevKey: 'lorem/orig',
prevParentPath: 'lorem',
};
@@ -602,7 +589,6 @@ describe('Multi-file store mutations', () => {
prevId: undefined,
prevPath: undefined,
prevName: undefined,
- prevUrl: undefined,
prevKey: undefined,
prevParentPath: undefined,
}),
diff --git a/spec/frontend/ide/sync_router_and_store_spec.js b/spec/frontend/ide/sync_router_and_store_spec.js
index ccf6e200806..20fd77c4dfb 100644
--- a/spec/frontend/ide/sync_router_and_store_spec.js
+++ b/spec/frontend/ide/sync_router_and_store_spec.js
@@ -17,9 +17,13 @@ describe('~/ide/sync_router_and_store', () => {
const getRouterCurrentPath = () => router.currentRoute.fullPath;
const getStoreCurrentPath = () => store.state.router.fullPath;
- const updateRouter = path => {
+ const updateRouter = async path => {
+ if (getRouterCurrentPath() === path) {
+ return;
+ }
+
router.push(path);
- return waitForPromises();
+ await waitForPromises();
};
const updateStore = path => {
store.dispatch('router/push', path);
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index e7ef0de45a0..97dc8217ecc 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -2,7 +2,7 @@ import { languages } from 'monaco-editor';
import {
isTextFile,
registerLanguages,
- registerSchemas,
+ registerSchema,
trimPathComponents,
insertFinalNewline,
trimTrailingWhitespace,
@@ -13,60 +13,78 @@ import {
describe('WebIDE utils', () => {
describe('isTextFile', () => {
- it('returns false for known binary types', () => {
- expect(isTextFile('file content', 'image/png', 'my.png')).toBeFalsy();
- // mime types are case insensitive
- expect(isTextFile('file content', 'IMAGE/PNG', 'my.png')).toBeFalsy();
+ it.each`
+ mimeType | name | type | result
+ ${'image/png'} | ${'my.png'} | ${'binary'} | ${false}
+ ${'IMAGE/PNG'} | ${'my.png'} | ${'binary'} | ${false}
+ ${'text/plain'} | ${'my.txt'} | ${'text'} | ${true}
+ ${'TEXT/PLAIN'} | ${'my.txt'} | ${'text'} | ${true}
+ `('returns $result for known $type types', ({ mimeType, name, result }) => {
+ expect(isTextFile({ content: 'file content', mimeType, name })).toBe(result);
});
- it('returns true for known text types', () => {
- expect(isTextFile('file content', 'text/plain', 'my.txt')).toBeTruthy();
- // mime types are case insensitive
- expect(isTextFile('file content', 'TEXT/PLAIN', 'my.txt')).toBeTruthy();
- });
+ it.each`
+ content | mimeType | name
+ ${'{"éêė":"value"}'} | ${'application/json'} | ${'my.json'}
+ ${'{"éêė":"value"}'} | ${'application/json'} | ${'.tsconfig'}
+ ${'SELECT "éêė" from tablename'} | ${'application/sql'} | ${'my.sql'}
+ ${'{"éêė":"value"}'} | ${'application/json'} | ${'MY.JSON'}
+ ${'SELECT "éêė" from tablename'} | ${'application/sql'} | ${'MY.SQL'}
+ ${'var code = "something"'} | ${'application/javascript'} | ${'Gruntfile'}
+ ${'MAINTAINER Александр "a21283@me.com"'} | ${'application/octet-stream'} | ${'dockerfile'}
+ `(
+ 'returns true for file extensions that Monaco supports syntax highlighting for',
+ ({ content, mimeType, name }) => {
+ expect(isTextFile({ content, mimeType, name })).toBe(true);
+ },
+ );
- it('returns true for file extensions that Monaco supports syntax highlighting for', () => {
- // test based on both MIME and extension
- expect(isTextFile('{"éêė":"value"}', 'application/json', 'my.json')).toBeTruthy();
- expect(isTextFile('{"éêė":"value"}', 'application/json', '.tsconfig')).toBeTruthy();
- expect(isTextFile('SELECT "éêė" from tablename', 'application/sql', 'my.sql')).toBeTruthy();
+ it('returns false if filename is same as the expected extension', () => {
+ expect(
+ isTextFile({
+ name: 'sql',
+ content: 'SELECT "éêė" from tablename',
+ mimeType: 'application/sql',
+ }),
+ ).toBeFalsy();
});
- it('returns true even irrespective of whether the mimes, extensions or file names are lowercase or upper case', () => {
- expect(isTextFile('{"éêė":"value"}', 'application/json', 'MY.JSON')).toBeTruthy();
- expect(isTextFile('SELECT "éêė" from tablename', 'application/sql', 'MY.SQL')).toBeTruthy();
- expect(
- isTextFile('var code = "something"', 'application/javascript', 'Gruntfile'),
- ).toBeTruthy();
+ it('returns true for ASCII only content for unknown types', () => {
expect(
- isTextFile(
- 'MAINTAINER Александр "alexander11354322283@me.com"',
- 'application/octet-stream',
- 'dockerfile',
- ),
+ isTextFile({
+ name: 'hello.mytype',
+ content: 'plain text',
+ mimeType: 'application/x-new-type',
+ }),
).toBeTruthy();
});
- it('returns false if filename is same as the expected extension', () => {
- expect(isTextFile('SELECT "éêė" from tablename', 'application/sql', 'sql')).toBeFalsy();
- });
-
- it('returns true for ASCII only content for unknown types', () => {
- expect(isTextFile('plain text', 'application/x-new-type', 'hello.mytype')).toBeTruthy();
+ it('returns false for non-ASCII content for unknown types', () => {
+ expect(
+ isTextFile({
+ name: 'my.random',
+ content: '{"éêė":"value"}',
+ mimeType: 'application/octet-stream',
+ }),
+ ).toBeFalsy();
});
- it('returns true for relevant filenames', () => {
- expect(
- isTextFile(
- 'MAINTAINER Александр "alexander11354322283@me.com"',
- 'application/octet-stream',
- 'Dockerfile',
- ),
- ).toBeTruthy();
+ it.each`
+ name | result
+ ${'myfile.txt'} | ${true}
+ ${'Dockerfile'} | ${true}
+ ${'img.png'} | ${false}
+ ${'abc.js'} | ${true}
+ ${'abc.random'} | ${false}
+ ${'image.jpeg'} | ${false}
+ `('returns $result for $filename when no content or mimeType is passed', ({ name, result }) => {
+ expect(isTextFile({ name })).toBe(result);
});
- it('returns false for non-ASCII content for unknown types', () => {
- expect(isTextFile('{"éêė":"value"}', 'application/octet-stream', 'my.random')).toBeFalsy();
+ it('returns true if content is empty string but false if content is not passed', () => {
+ expect(isTextFile({ name: 'abc.dat' })).toBe(false);
+ expect(isTextFile({ name: 'abc.dat', content: '' })).toBe(true);
+ expect(isTextFile({ name: 'abc.dat', content: ' ' })).toBe(true);
});
});
@@ -159,55 +177,37 @@ describe('WebIDE utils', () => {
});
});
- describe('registerSchemas', () => {
- let options;
+ describe('registerSchema', () => {
+ let schema;
beforeEach(() => {
- options = {
- validate: true,
- enableSchemaRequest: true,
- hover: true,
- completion: true,
- schemas: [
- {
- uri: 'http://myserver/foo-schema.json',
- fileMatch: ['*'],
- schema: {
- id: 'http://myserver/foo-schema.json',
- type: 'object',
- properties: {
- p1: { enum: ['v1', 'v2'] },
- p2: { $ref: 'http://myserver/bar-schema.json' },
- },
- },
- },
- {
- uri: 'http://myserver/bar-schema.json',
- schema: {
- id: 'http://myserver/bar-schema.json',
- type: 'object',
- properties: { q1: { enum: ['x1', 'x2'] } },
- },
+ schema = {
+ uri: 'http://myserver/foo-schema.json',
+ fileMatch: ['*'],
+ schema: {
+ id: 'http://myserver/foo-schema.json',
+ type: 'object',
+ properties: {
+ p1: { enum: ['v1', 'v2'] },
+ p2: { $ref: 'http://myserver/bar-schema.json' },
},
- ],
+ },
};
jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions');
jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
});
- it.each`
- language | defaultsObj
- ${'json'} | ${languages.json.jsonDefaults}
- ${'yaml'} | ${languages.yaml.yamlDefaults}
- `(
- 'registers the given schemas with monaco for lang: $language',
- ({ language, defaultsObj }) => {
- registerSchemas({ language, options });
+ it('registers the given schemas with monaco for both json and yaml languages', () => {
+ registerSchema(schema);
- expect(defaultsObj.setDiagnosticsOptions).toHaveBeenCalledWith(options);
- },
- );
+ expect(languages.json.jsonDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
+ expect.objectContaining({ schemas: [schema] }),
+ );
+ expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
+ expect.objectContaining({ schemas: [schema] }),
+ );
+ });
});
describe('trimTrailingWhitespace', () => {
diff --git a/spec/frontend/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_projects/components/bitbucket_status_table_spec.js
index 132ccd0e324..b65b388fd5f 100644
--- a/spec/frontend/import_projects/components/bitbucket_status_table_spec.js
+++ b/spec/frontend/import_projects/components/bitbucket_status_table_spec.js
@@ -33,7 +33,7 @@ describe('BitbucketStatusTable', () => {
it('renders import table component', () => {
createComponent({ providerTitle: 'Test' });
- expect(wrapper.contains(ImportProjectsTable)).toBe(true);
+ expect(wrapper.find(ImportProjectsTable).exists()).toBe(true);
});
it('passes alert in incompatible-repos-warning slot', () => {
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
index b217242968a..1dbad588ec4 100644
--- a/spec/frontend/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -1,15 +1,12 @@
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon, GlButton } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
import state from '~/import_projects/store/state';
import * as getters from '~/import_projects/store/getters';
import { STATUSES } from '~/import_projects/constants';
import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
-import IncompatibleRepoTableRow from '~/import_projects/components/incompatible_repo_table_row.vue';
-import PageQueryParamSync from '~/import_projects/components/page_query_param_sync.vue';
describe('ImportProjectsTable', () => {
let wrapper;
@@ -18,16 +15,26 @@ describe('ImportProjectsTable', () => {
wrapper.find('input[data-qa-selector="githubish_import_filter_field"]');
const providerTitle = 'THE PROVIDER';
- const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
+ const providerRepo = {
+ importSource: {
+ id: 10,
+ sanitizedName: 'sanitizedName',
+ fullName: 'fullName',
+ },
+ importedProject: null,
+ };
const findImportAllButton = () =>
wrapper
.findAll(GlButton)
.filter(w => w.props().variant === 'success')
.at(0);
+ const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
const importAllFn = jest.fn();
+ const importAllModalShowFn = jest.fn();
const setPageFn = jest.fn();
+ const fetchReposFn = jest.fn();
function createComponent({
state: initialState,
@@ -46,7 +53,7 @@ describe('ImportProjectsTable', () => {
...customGetters,
},
actions: {
- fetchRepos: jest.fn(),
+ fetchRepos: fetchReposFn,
fetchJobs: jest.fn(),
fetchNamespaces: jest.fn(),
importAll: importAllFn,
@@ -66,6 +73,9 @@ describe('ImportProjectsTable', () => {
paginatable,
},
slots,
+ stubs: {
+ GlModal: { template: '<div>Modal!</div>', methods: { show: importAllModalShowFn } },
+ },
});
}
@@ -79,58 +89,54 @@ describe('ImportProjectsTable', () => {
it('renders a loading icon while repos are loading', () => {
createComponent({ state: { isLoadingRepos: true } });
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('renders a loading icon while namespaces are loading', () => {
createComponent({ state: { isLoadingNamespaces: true } });
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('renders a table with imported projects and provider repos', () => {
+ it('renders a table with provider repos', () => {
+ const repositories = [
+ { importSource: { id: 1 }, importedProject: null },
+ { importSource: { id: 2 }, importedProject: { importStatus: STATUSES.FINISHED } },
+ { importSource: { id: 3, incompatible: true }, importedProject: {} },
+ ];
+
createComponent({
- state: {
- namespaces: [{ fullPath: 'path' }],
- repositories: [
- { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
- { importSource: { id: 2 }, importedProject: {}, importStatus: STATUSES.FINISHED },
- {
- importSource: { id: 3, incompatible: true },
- importedProject: {},
- importStatus: STATUSES.NONE,
- },
- ],
- },
+ state: { namespaces: [{ fullPath: 'path' }], repositories },
});
- expect(wrapper.contains(GlLoadingIcon)).toBe(false);
- expect(wrapper.contains('table')).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('table').exists()).toBe(true);
expect(
wrapper
.findAll('th')
.filter(w => w.text() === `From ${providerTitle}`)
- .isEmpty(),
- ).toBe(false);
+ .exists(),
+ ).toBe(true);
- expect(wrapper.contains(ProviderRepoTableRow)).toBe(true);
- expect(wrapper.contains(ImportedProjectTableRow)).toBe(true);
- expect(wrapper.contains(IncompatibleRepoTableRow)).toBe(true);
+ expect(wrapper.findAll(ProviderRepoTableRow)).toHaveLength(repositories.length);
});
it.each`
- hasIncompatibleRepos | buttonText
- ${false} | ${'Import all repositories'}
- ${true} | ${'Import all compatible repositories'}
+ hasIncompatibleRepos | count | buttonText
+ ${false} | ${1} | ${'Import 1 repository'}
+ ${true} | ${1} | ${'Import 1 compatible repository'}
+ ${false} | ${5} | ${'Import 5 repositories'}
+ ${true} | ${5} | ${'Import 5 compatible repositories'}
`(
- 'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos',
- ({ hasIncompatibleRepos, buttonText }) => {
+ 'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos and repos count is $count',
+ ({ hasIncompatibleRepos, buttonText, count }) => {
createComponent({
state: {
providerRepos: [providerRepo],
},
getters: {
hasIncompatibleRepos: () => hasIncompatibleRepos,
+ importAllCount: () => count,
},
});
@@ -138,20 +144,28 @@ describe('ImportProjectsTable', () => {
},
);
- it('renders an empty state if there are no projects available', () => {
+ it('renders an empty state if there are no repositories available', () => {
createComponent({ state: { repositories: [] } });
- expect(wrapper.contains(ProviderRepoTableRow)).toBe(false);
- expect(wrapper.contains(ImportedProjectTableRow)).toBe(false);
+ expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
- it('sends importAll event when import button is clicked', async () => {
- createComponent({ state: { providerRepos: [providerRepo] } });
+ it('opens confirmation modal when import all button is clicked', async () => {
+ createComponent({ state: { repositories: [providerRepo] } });
findImportAllButton().vm.$emit('click');
await nextTick();
+ expect(importAllModalShowFn).toHaveBeenCalled();
+ });
+
+ it('triggers importAll action when modal is confirmed', async () => {
+ createComponent({ state: { providerRepos: [providerRepo] } });
+
+ findImportAllModal().vm.$emit('ok');
+ await nextTick();
+
expect(importAllFn).toHaveBeenCalled();
});
@@ -189,21 +203,29 @@ describe('ImportProjectsTable', () => {
});
});
- it('passes current page to page-query-param-sync component', () => {
- expect(wrapper.find(PageQueryParamSync).props().page).toBe(pageInfo.page);
+ it('does not call fetchRepos on mount', () => {
+ expect(fetchReposFn).not.toHaveBeenCalled();
+ });
+
+ it('renders intersection observer component', () => {
+ expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
});
- it('dispatches setPage when page-query-param-sync emits popstate', () => {
- const NEW_PAGE = 2;
- wrapper.find(PageQueryParamSync).vm.$emit('popstate', NEW_PAGE);
+ it('calls fetchRepos when intersection observer appears', async () => {
+ wrapper.find(GlIntersectionObserver).vm.$emit('appear');
- const { calls } = setPageFn.mock;
+ await nextTick();
- expect(calls).toHaveLength(1);
- expect(calls[0][1]).toBe(NEW_PAGE);
+ expect(fetchReposFn).toHaveBeenCalled();
});
});
+ it('calls fetchRepos on mount', () => {
+ createComponent();
+
+ expect(fetchReposFn).toHaveBeenCalled();
+ });
+
it.each`
hasIncompatibleRepos | shouldRenderSlot | action
${false} | ${false} | ${'does not render'}
diff --git a/spec/frontend/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
deleted file mode 100644
index 8890c352826..00000000000
--- a/spec/frontend/import_projects/components/imported_project_table_row_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { mount } from '@vue/test-utils';
-import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
-import ImportStatus from '~/import_projects/components/import_status.vue';
-import { STATUSES } from '~/import_projects/constants';
-
-describe('ImportedProjectTableRow', () => {
- let wrapper;
- const project = {
- importSource: {
- fullName: 'fullName',
- providerLink: 'providerLink',
- },
- importedProject: {
- id: 1,
- fullPath: 'fullPath',
- importSource: 'importSource',
- },
- importStatus: STATUSES.FINISHED,
- };
-
- function mountComponent() {
- wrapper = mount(ImportedProjectTableRow, { propsData: { project } });
- }
-
- beforeEach(() => {
- mountComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders an imported project table row', () => {
- const providerLink = wrapper.find('[data-testid=providerLink]');
-
- expect(providerLink.attributes().href).toMatch(project.importSource.providerLink);
- expect(providerLink.text()).toMatch(project.importSource.fullName);
- expect(wrapper.find('[data-testid=fullPath]').text()).toMatch(project.importedProject.fullPath);
- expect(wrapper.find(ImportStatus).props().status).toBe(project.importStatus);
- expect(wrapper.find('[data-testid=goToProject').attributes().href).toMatch(
- project.importedProject.fullPath,
- );
- });
-});
diff --git a/spec/frontend/import_projects/components/page_query_param_sync_spec.js b/spec/frontend/import_projects/components/page_query_param_sync_spec.js
deleted file mode 100644
index be19ecca1ba..00000000000
--- a/spec/frontend/import_projects/components/page_query_param_sync_spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-
-import PageQueryParamSync from '~/import_projects/components/page_query_param_sync.vue';
-
-describe('PageQueryParamSync', () => {
- let originalPushState;
- let originalAddEventListener;
- let originalRemoveEventListener;
-
- const pushStateMock = jest.fn();
- const addEventListenerMock = jest.fn();
- const removeEventListenerMock = jest.fn();
-
- beforeAll(() => {
- window.location.search = '';
- originalPushState = window.pushState;
-
- window.history.pushState = pushStateMock;
-
- originalAddEventListener = window.addEventListener;
- window.addEventListener = addEventListenerMock;
-
- originalRemoveEventListener = window.removeEventListener;
- window.removeEventListener = removeEventListenerMock;
- });
-
- afterAll(() => {
- window.history.pushState = originalPushState;
- window.addEventListener = originalAddEventListener;
- window.removeEventListener = originalRemoveEventListener;
- });
-
- let wrapper;
- beforeEach(() => {
- wrapper = shallowMount(PageQueryParamSync, {
- propsData: { page: 3 },
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('calls push state with page number when page is updated and differs from 1', async () => {
- wrapper.setProps({ page: 2 });
-
- await nextTick();
-
- const { calls } = pushStateMock.mock;
- expect(calls).toHaveLength(1);
- expect(calls[0][2]).toBe(`${TEST_HOST}/?page=2`);
- });
-
- it('calls push state without page number when page is updated and is 1', async () => {
- wrapper.setProps({ page: 1 });
-
- await nextTick();
-
- const { calls } = pushStateMock.mock;
- expect(calls).toHaveLength(1);
- expect(calls[0][2]).toBe(`${TEST_HOST}/`);
- });
-
- it('subscribes to popstate event on create', () => {
- expect(addEventListenerMock).toHaveBeenCalledWith('popstate', expect.any(Function));
- });
-
- it('unsubscribes from popstate event when destroyed', () => {
- const [, fn] = addEventListenerMock.mock.calls[0];
-
- wrapper.destroy();
-
- expect(removeEventListenerMock).toHaveBeenCalledWith('popstate', fn);
- });
-
- it('emits popstate event when popstate is triggered', async () => {
- const [, fn] = addEventListenerMock.mock.calls[0];
-
- delete window.location;
- window.location = new URL(`${TEST_HOST}/?page=5`);
- fn();
-
- expect(wrapper.emitted().popstate[0]).toStrictEqual([5]);
- });
-});
diff --git a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index bd9cd07db78..03e30ef610e 100644
--- a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -1,6 +1,7 @@
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlBadge } from '@gitlab/ui';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import ImportStatus from '~/import_projects/components/import_status.vue';
import { STATUSES } from '~/import_projects/constants';
@@ -14,20 +15,6 @@ describe('ProviderRepoTableRow', () => {
targetNamespace: 'target',
newName: 'newName',
};
- const ciCdOnly = false;
- const repo = {
- importSource: {
- id: 'remote-1',
- fullName: 'fullName',
- providerLink: 'providerLink',
- },
- importedProject: {
- id: 1,
- fullPath: 'fullPath',
- importSource: 'importSource',
- },
- importStatus: STATUSES.FINISHED,
- };
const availableNamespaces = [
{ text: 'Groups', children: [{ id: 'test', text: 'test' }] },
@@ -46,55 +33,137 @@ describe('ProviderRepoTableRow', () => {
return store;
}
- const findImportButton = () =>
- wrapper
- .findAll('button')
- .filter(node => node.text() === 'Import')
- .at(0);
+ const findImportButton = () => {
+ const buttons = wrapper.findAll('button').filter(node => node.text() === 'Import');
+
+ return buttons.length ? buttons.at(0) : buttons;
+ };
- function mountComponent(initialState) {
+ function mountComponent(props) {
const localVue = createLocalVue();
localVue.use(Vuex);
- const store = initStore({ ciCdOnly, ...initialState });
+ const store = initStore();
wrapper = shallowMount(ProviderRepoTableRow, {
localVue,
store,
- propsData: { repo, availableNamespaces },
+ propsData: { availableNamespaces, ...props },
});
}
- beforeEach(() => {
- mountComponent();
- });
-
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
- it('renders a provider repo table row', () => {
- const providerLink = wrapper.find('[data-testid=providerLink]');
+ describe('when rendering importable project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
+
+ it('renders empty import status', () => {
+ expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
+ });
+
+ it('renders a select2 namespace select', () => {
+ expect(wrapper.find(Select2Select).exists()).toBe(true);
+ expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
+ });
+
+ it('renders import button', () => {
+ expect(findImportButton().exists()).toBe(true);
+ });
+
+ it('imports repo when clicking import button', async () => {
+ findImportButton().trigger('click');
+
+ await nextTick();
+
+ const { calls } = fetchImport.mock;
- expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
- expect(providerLink.text()).toMatch(repo.importSource.fullName);
- expect(wrapper.find(ImportStatus).props().status).toBe(repo.importStatus);
- expect(wrapper.contains('button')).toBe(true);
+ expect(calls).toHaveLength(1);
+ expect(calls[0][1]).toBe(repo.importSource.id);
+ });
});
- it('renders a select2 namespace select', () => {
- expect(wrapper.contains(Select2Select)).toBe(true);
- expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
+ describe('when rendering imported project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ importStatus: STATUSES.FINISHED,
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
+
+ it('renders proper import status', () => {
+ expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
+ });
+
+ it('does not renders a namespace select', () => {
+ expect(wrapper.find(Select2Select).exists()).toBe(false);
+ });
+
+ it('does not render import button', () => {
+ expect(findImportButton().exists()).toBe(false);
+ });
});
- it('imports repo when clicking import button', async () => {
- findImportButton().trigger('click');
+ describe('when rendering incompatible project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ incompatible: true,
+ },
+ };
- await nextTick();
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
- const { calls } = fetchImport.mock;
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
- expect(calls).toHaveLength(1);
- expect(calls[0][1]).toBe(repo.importSource.id);
+ it('renders badge with error', () => {
+ expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
+ });
});
});
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 45a59b3f6d6..6951f2bf04d 100644
--- a/spec/frontend/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -83,7 +83,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('dispatches stopJobsPolling actions and commits REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
+ it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(200, payload);
return testAction(
@@ -91,54 +91,65 @@ describe('import_projects store actions', () => {
null,
localState,
[
+ { type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
},
],
- [{ type: 'stopJobsPolling' }, { type: 'fetchJobs' }],
+ [],
);
});
- it('dispatches stopJobsPolling action and commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
+ it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_ERROR and SET_PAGE again mutations on an unsuccessful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
return testAction(
fetchRepos,
null,
localState,
- [{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
- [{ type: 'stopJobsPolling' }],
+ [
+ { type: SET_PAGE, payload: 1 },
+ { type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 0 },
+ { type: RECEIVE_REPOS_ERROR },
+ ],
+ [],
);
});
- describe('when pagination is enabled', () => {
- it('includes page in url query params', async () => {
- const { fetchRepos: fetchReposWithPagination } = actionsFactory({
- endpoints,
- hasPagination: true,
- });
+ it('includes page in url query params', async () => {
+ let requestedUrl;
+ mock.onGet().reply(config => {
+ requestedUrl = config.url;
+ return [200, payload];
+ });
- let requestedUrl;
- mock.onGet().reply(config => {
- requestedUrl = config.url;
- return [200, payload];
- });
+ const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
- await testAction(
- fetchReposWithPagination,
- null,
- localState,
- expect.any(Array),
- expect.any(Array),
- );
+ await testAction(fetchRepos, null, localStateWithPage, expect.any(Array), expect.any(Array));
- expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localState.pageInfo.page}`);
- });
+ expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
});
- describe('when filtered', () => {
+ it('correctly updates current page on an unsuccessful request', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(500);
+ const CURRENT_PAGE = 5;
+
+ return testAction(
+ fetchRepos,
+ null,
+ { ...localState, pageInfo: { page: CURRENT_PAGE } },
+ expect.arrayContaining([
+ { type: SET_PAGE, payload: CURRENT_PAGE + 1 },
+ { type: SET_PAGE, payload: CURRENT_PAGE },
+ ]),
+ [],
+ );
+ });
+
+ describe('when /home/xanf/projects/gdk/gitlab/spec/frontend/import_projects/store/actions_spec.jsfiltered', () => {
it('fetches repos with filter applied', () => {
mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
@@ -147,13 +158,14 @@ describe('import_projects store actions', () => {
null,
{ ...localState, filter: 'filter' },
[
+ { type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
},
],
- [{ type: 'stopJobsPolling' }, { type: 'fetchJobs' }],
+ [],
);
});
});
diff --git a/spec/frontend/import_projects/store/getters_spec.js b/spec/frontend/import_projects/store/getters_spec.js
index 5c1ea25a684..1ce42e534ea 100644
--- a/spec/frontend/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_projects/store/getters_spec.js
@@ -3,6 +3,7 @@ import {
isImportingAnyRepo,
hasIncompatibleRepos,
hasImportableRepos,
+ importAllCount,
getImportTarget,
} from '~/import_projects/store/getters';
import { STATUSES } from '~/import_projects/constants';
@@ -10,13 +11,12 @@ import state from '~/import_projects/store/state';
const IMPORTED_REPO = {
importSource: {},
- importedProject: { fullPath: 'some/path' },
+ importedProject: { fullPath: 'some/path', importStatus: STATUSES.FINISHED },
};
const IMPORTABLE_REPO = {
importSource: { id: 'some-id', sanitizedName: 'sanitized' },
importedProject: null,
- importStatus: STATUSES.NONE,
};
const INCOMPATIBLE_REPO = {
@@ -56,14 +56,20 @@ describe('import_projects store getters', () => {
${STATUSES.STARTED} | ${true}
${STATUSES.FINISHED} | ${false}
`(
- 'isImportingAnyRepo returns $value when repo with $importStatus status is available',
+ 'isImportingAnyRepo returns $value when project with $importStatus status is available',
({ importStatus, value }) => {
- localState.repositories = [{ importStatus }];
+ localState.repositories = [{ importedProject: { importStatus } }];
expect(isImportingAnyRepo(localState)).toBe(value);
},
);
+ it('isImportingAnyRepo returns false when project with no defined importStatus status is available', () => {
+ localState.repositories = [{ importSource: {} }];
+
+ expect(isImportingAnyRepo(localState)).toBe(false);
+ });
+
describe('hasIncompatibleRepos', () => {
it('returns true if there are any incompatible projects', () => {
localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
@@ -92,6 +98,19 @@ describe('import_projects store getters', () => {
});
});
+ describe('importAllCount', () => {
+ it('returns count of available importable projects ', () => {
+ localState.repositories = [
+ IMPORTABLE_REPO,
+ IMPORTABLE_REPO,
+ IMPORTED_REPO,
+ INCOMPATIBLE_REPO,
+ ];
+
+ expect(importAllCount(localState)).toBe(2);
+ });
+ });
+
describe('getImportTarget', () => {
it('returns default value if no custom target available', () => {
localState.defaultTargetNamespace = 'default';
diff --git a/spec/frontend/import_projects/store/mutations_spec.js b/spec/frontend/import_projects/store/mutations_spec.js
index 3672ec9f2c0..5d78a7fa9e7 100644
--- a/spec/frontend/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_projects/store/mutations_spec.js
@@ -1,9 +1,11 @@
import * as types from '~/import_projects/store/mutation_types';
import mutations from '~/import_projects/store/mutations';
+import getInitialState from '~/import_projects/store/state';
import { STATUSES } from '~/import_projects/constants';
describe('import_projects store mutations', () => {
let state;
+
const SOURCE_PROJECT = {
id: 1,
full_name: 'full/name',
@@ -19,13 +21,23 @@ describe('import_projects store mutations', () => {
};
describe(`${types.SET_FILTER}`, () => {
- it('overwrites current filter value', () => {
- state = { filter: 'some-value' };
- const NEW_VALUE = 'new-value';
+ const NEW_VALUE = 'new-value';
+ beforeEach(() => {
+ state = {
+ filter: 'some-value',
+ repositories: ['some', ' repositories'],
+ pageInfo: { page: 1 },
+ };
mutations[types.SET_FILTER](state, NEW_VALUE);
+ });
- expect(state.filter).toBe(NEW_VALUE);
+ it('removes current repositories list', () => {
+ expect(state.repositories.length).toBe(0);
+ });
+
+ it('resets current page to 0', () => {
+ expect(state.pageInfo.page).toBe(0);
});
});
@@ -40,93 +52,104 @@ describe('import_projects store mutations', () => {
});
describe(`${types.RECEIVE_REPOS_SUCCESS}`, () => {
- describe('for imported projects', () => {
- const response = {
- importedProjects: [IMPORTED_PROJECT],
- providerRepos: [],
- };
+ describe('with legacy response format', () => {
+ describe('for imported projects', () => {
+ const response = {
+ importedProjects: [IMPORTED_PROJECT],
+ providerRepos: [],
+ };
- it('picks import status from response', () => {
- state = {};
+ it('recreates importSource from response', () => {
+ state = getInitialState();
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
- });
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining({
+ fullName: IMPORTED_PROJECT.importSource,
+ sanitizedName: IMPORTED_PROJECT.name,
+ providerLink: IMPORTED_PROJECT.providerLink,
+ }),
+ );
+ });
- it('recreates importSource from response', () => {
- state = {};
+ it('passes project to importProject', () => {
+ state = getInitialState();
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- expect(state.repositories[0].importSource).toStrictEqual(
- expect.objectContaining({
- fullName: IMPORTED_PROJECT.importSource,
- sanitizedName: IMPORTED_PROJECT.name,
- providerLink: IMPORTED_PROJECT.providerLink,
- }),
- );
+ expect(IMPORTED_PROJECT).toStrictEqual(
+ expect.objectContaining(state.repositories[0].importedProject),
+ );
+ });
});
- it('passes project to importProject', () => {
- state = {};
+ describe('for importable projects', () => {
+ beforeEach(() => {
+ state = getInitialState();
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ const response = {
+ importedProjects: [],
+ providerRepos: [SOURCE_PROJECT],
+ };
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
- expect(IMPORTED_PROJECT).toStrictEqual(
- expect.objectContaining(state.repositories[0].importedProject),
- );
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
+ });
});
- });
- describe('for importable projects', () => {
- beforeEach(() => {
- state = {};
+ describe('for incompatible projects', () => {
const response = {
importedProjects: [],
- providerRepos: [SOURCE_PROJECT],
+ providerRepos: [],
+ incompatibleRepos: [SOURCE_PROJECT],
};
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- });
- it('sets import status to none', () => {
- expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
- });
+ beforeEach(() => {
+ state = getInitialState();
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
+
+ it('sets incompatible flag', () => {
+ expect(state.repositories[0].importSource.incompatible).toBe(true);
+ });
- it('sets importSource to project', () => {
- expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining(SOURCE_PROJECT),
+ );
+ });
});
- });
- describe('for incompatible projects', () => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- incompatibleRepos: [SOURCE_PROJECT],
- };
+ it('sets repos loading flag to false', () => {
+ const response = {
+ importedProjects: [],
+ providerRepos: [],
+ };
+
+ state = getInitialState();
- beforeEach(() => {
- state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- });
- it('sets incompatible flag', () => {
- expect(state.repositories[0].importSource.incompatible).toBe(true);
+ expect(state.isLoadingRepos).toBe(false);
});
+ });
- it('sets importSource to project', () => {
- expect(state.repositories[0].importSource).toStrictEqual(
- expect.objectContaining(SOURCE_PROJECT),
- );
- });
+ it('passes response as it is', () => {
+ const response = [];
+ state = getInitialState();
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.repositories).toStrictEqual(response);
});
it('sets repos loading flag to false', () => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- };
- state = {};
+ const response = [];
+
+ state = getInitialState();
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
@@ -136,7 +159,7 @@ describe('import_projects store mutations', () => {
describe(`${types.RECEIVE_REPOS_ERROR}`, () => {
it('sets repos loading flag to false', () => {
- state = {};
+ state = getInitialState();
mutations[types.RECEIVE_REPOS_ERROR](state);
@@ -154,7 +177,7 @@ describe('import_projects store mutations', () => {
});
it(`sets status to ${STATUSES.SCHEDULING}`, () => {
- expect(state.repositories[0].importStatus).toBe(STATUSES.SCHEDULING);
+ expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.SCHEDULING);
});
});
@@ -170,7 +193,9 @@ describe('import_projects store mutations', () => {
});
it('sets import status', () => {
- expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
+ expect(state.repositories[0].importedProject.importStatus).toBe(
+ IMPORTED_PROJECT.importStatus,
+ );
});
it('sets imported project', () => {
@@ -188,8 +213,8 @@ describe('import_projects store mutations', () => {
mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
});
- it(`resets import status to ${STATUSES.NONE}`, () => {
- expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
+ it(`removes importedProject entry`, () => {
+ expect(state.repositories[0].importedProject).toBeNull();
});
});
@@ -203,7 +228,9 @@ describe('import_projects store mutations', () => {
mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
- expect(state.repositories[0].importStatus).toBe(updatedProjects[0].importStatus);
+ expect(state.repositories[0].importedProject.importStatus).toBe(
+ updatedProjects[0].importStatus,
+ );
});
});
@@ -280,17 +307,6 @@ describe('import_projects store mutations', () => {
});
});
- describe(`${types.SET_PAGE_INFO}`, () => {
- it('sets passed page info', () => {
- state = {};
- const pageInfo = { page: 1, total: 10 };
-
- mutations[types.SET_PAGE_INFO](state, pageInfo);
-
- expect(state.pageInfo).toBe(pageInfo);
- });
- });
-
describe(`${types.SET_PAGE}`, () => {
it('sets page number', () => {
const NEW_PAGE = 4;
diff --git a/spec/frontend/import_projects/utils_spec.js b/spec/frontend/import_projects/utils_spec.js
index 826b06d5a70..4e1e16a3184 100644
--- a/spec/frontend/import_projects/utils_spec.js
+++ b/spec/frontend/import_projects/utils_spec.js
@@ -1,7 +1,16 @@
-import { isProjectImportable } from '~/import_projects/utils';
+import { isProjectImportable, isIncompatible, getImportStatus } from '~/import_projects/utils';
import { STATUSES } from '~/import_projects/constants';
describe('import_projects utils', () => {
+ const COMPATIBLE_PROJECT = {
+ importSource: { incompatible: false },
+ };
+
+ const INCOMPATIBLE_PROJECT = {
+ importSource: { incompatible: true },
+ importedProject: null,
+ };
+
describe('isProjectImportable', () => {
it.each`
status | result
@@ -14,19 +23,43 @@ describe('import_projects utils', () => {
`('returns $result when project is compatible and status is $status', ({ status, result }) => {
expect(
isProjectImportable({
- importStatus: status,
- importSource: { incompatible: false },
+ ...COMPATIBLE_PROJECT,
+ importedProject: { importStatus: status },
}),
).toBe(result);
});
+ it('returns true if import status is not defined', () => {
+ expect(isProjectImportable({ importSource: {} })).toBe(true);
+ });
+
it('returns false if project is not compatible', () => {
+ expect(isProjectImportable(INCOMPATIBLE_PROJECT)).toBe(false);
+ });
+ });
+
+ describe('isIncompatible', () => {
+ it('returns true for incompatible project', () => {
+ expect(isIncompatible(INCOMPATIBLE_PROJECT)).toBe(true);
+ });
+
+ it('returns false for compatible project', () => {
+ expect(isIncompatible(COMPATIBLE_PROJECT)).toBe(false);
+ });
+ });
+
+ describe('getImportStatus', () => {
+ it('returns actual status when project status is provided', () => {
expect(
- isProjectImportable({
- importStatus: STATUSES.NONE,
- importSource: { incompatible: true },
+ getImportStatus({
+ ...COMPATIBLE_PROJECT,
+ importedProject: { importStatus: STATUSES.FINISHED },
}),
- ).toBe(false);
+ ).toBe(STATUSES.FINISHED);
+ });
+
+ it('returns NONE as status if import status is not provided', () => {
+ expect(getImportStatus(COMPATIBLE_PROJECT)).toBe(STATUSES.NONE);
});
});
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 33ddd06d6d9..307806e0a8a 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -13,6 +13,7 @@ import {
} from '@gitlab/ui';
import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
import IncidentsList from '~/incidents/components/incidents_list.vue';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants';
import mockIncidents from '../mocks/incidents.json';
@@ -30,9 +31,9 @@ describe('Incidents List', () => {
const incidentTemplateName = 'incident';
const incidentType = 'incident';
const incidentsCount = {
- opened: 14,
- closed: 1,
- all: 16,
+ opened: 24,
+ closed: 10,
+ all: 26,
};
const findTable = () => wrapper.find(GlTable);
@@ -51,6 +52,7 @@ describe('Incidents List', () => {
const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findStatusTabs = () => wrapper.find(GlTabs);
const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findSeverity = () => wrapper.findAll(SeverityToken);
function mountComponent({ data = { incidents: [], incidentsCount: {} }, loading = false }) {
wrapper = mount(IncidentsList, {
@@ -78,6 +80,7 @@ describe('Incidents List', () => {
stubs: {
GlButton: true,
GlAvatar: true,
+ GlEmptyState: true,
},
});
}
@@ -96,12 +99,30 @@ describe('Incidents List', () => {
expect(findLoader().exists()).toBe(true);
});
- it('shows empty state', () => {
- mountComponent({
- data: { incidents: { list: [] }, incidentsCount: {} },
- loading: false,
- });
- expect(findEmptyState().exists()).toBe(true);
+ describe('empty state', () => {
+ const {
+ emptyState: { title, emptyClosedTabTitle, description },
+ } = I18N;
+
+ it.each`
+ statusFilter | all | closed | expectedTitle | expectedDescription
+ ${'all'} | ${2} | ${1} | ${title} | ${description}
+ ${'open'} | ${2} | ${0} | ${title} | ${description}
+ ${'closed'} | ${0} | ${0} | ${title} | ${description}
+ ${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${undefined}
+ `(
+ `when active tab is $statusFilter and there are $all incidents in total and $closed closed incidents, the empty state
+ has title: $expectedTitle and description: $expectedDescription`,
+ ({ statusFilter, all, closed, expectedTitle, expectedDescription }) => {
+ mountComponent({
+ data: { incidents: { list: [] }, incidentsCount: { all, closed }, statusFilter },
+ loading: false,
+ });
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().attributes('title')).toBe(expectedTitle);
+ expect(findEmptyState().attributes('description')).toBe(expectedDescription);
+ },
+ );
});
it('shows error state', () => {
@@ -163,6 +184,10 @@ describe('Incidents List', () => {
);
});
});
+
+ it('renders severity per row', () => {
+ expect(findSeverity().length).toBe(mockIncidents.length);
+ });
});
describe('Create Incident', () => {
@@ -188,6 +213,14 @@ describe('Incidents List', () => {
expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
});
});
+
+ it("doesn't show the button when list is empty", () => {
+ mountComponent({
+ data: { incidents: { list: [] }, incidentsCount: {} },
+ loading: false,
+ });
+ expect(findCreateIncidentBtn().exists()).toBe(false);
+ });
});
describe('Pagination', () => {
@@ -313,7 +346,7 @@ describe('Incidents List', () => {
describe('Status Filter Tabs', () => {
beforeEach(() => {
mountComponent({
- data: { incidents: mockIncidents, incidentsCount },
+ data: { incidents: { list: mockIncidents }, incidentsCount },
loading: false,
stubs: {
GlTab: true,
@@ -345,7 +378,7 @@ describe('Incidents List', () => {
describe('sorting the incident list by column', () => {
beforeEach(() => {
mountComponent({
- data: { incidents: mockIncidents, incidentsCount },
+ data: { incidents: { list: mockIncidents }, incidentsCount },
loading: false,
});
});
diff --git a/spec/frontend/incidents/mocks/incidents.json b/spec/frontend/incidents/mocks/incidents.json
index 4eab709e53f..42b3d6d3eb6 100644
--- a/spec/frontend/incidents/mocks/incidents.json
+++ b/spec/frontend/incidents/mocks/incidents.json
@@ -4,7 +4,8 @@
"title": "New: Incident",
"createdAt": "2020-06-03T15:46:08Z",
"assignees": {},
- "state": "opened"
+ "state": "opened",
+ "severity": "CRITICAL"
},
{
"iid": "14",
@@ -20,20 +21,23 @@
}
]
},
- "state": "opened"
+ "state": "opened",
+ "severity": "HIGH"
},
{
"iid": "13",
"title": "Create issue3",
"createdAt": "2020-05-19T08:53:55Z",
"assignees": {},
- "state": "closed"
+ "state": "closed",
+ "severity": "LOW"
},
{
"iid": "12",
"title": "Create issue2",
"createdAt": "2020-05-18T17:13:35Z",
"assignees": {},
- "state": "closed"
+ "state": "closed",
+ "severity": "MEDIUM"
}
]
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
index f3f610e4bb7..cab2165b5db 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -45,7 +45,7 @@ exports[`Alert integration settings form default state should match the default
</gl-link-stub>
</label>
- <gl-new-dropdown-stub
+ <gl-dropdown-stub
block="true"
category="tertiary"
data-qa-selector="incident_templates_dropdown"
@@ -55,7 +55,7 @@ exports[`Alert integration settings form default state should match the default
text="selecte_tmpl"
variant="default"
>
- <gl-new-dropdown-item-stub
+ <gl-dropdown-item-stub
avatarurl=""
data-qa-selector="incident_templates_item"
iconcolor=""
@@ -67,8 +67,8 @@ exports[`Alert integration settings form default state should match the default
No template selected
- </gl-new-dropdown-item-stub>
- </gl-new-dropdown-stub>
+ </gl-dropdown-item-stub>
+ </gl-dropdown-stub>
</gl-form-group-stub>
<gl-form-group-stub
@@ -81,6 +81,18 @@ exports[`Alert integration settings form default state should match the default
</gl-form-checkbox-stub>
</gl-form-group-stub>
+ <gl-form-group-stub
+ class="gl-pl-0 gl-mb-5"
+ >
+ <gl-form-checkbox-stub
+ checked="true"
+ >
+ <span>
+ Automatically close incident issues when the associated Prometheus alert resolves.
+ </span>
+ </gl-form-checkbox-stub>
+ </gl-form-group-stub>
+
<div
class="gl-display-flex gl-justify-content-end"
>
diff --git a/spec/frontend/incidents_settings/components/alerts_form_spec.js b/spec/frontend/incidents_settings/components/alerts_form_spec.js
index 04832f31e58..2516e8afdfa 100644
--- a/spec/frontend/incidents_settings/components/alerts_form_spec.js
+++ b/spec/frontend/incidents_settings/components/alerts_form_spec.js
@@ -16,6 +16,7 @@ describe('Alert integration settings form', () => {
createIssue: true,
sendEmail: false,
templates: [],
+ autoCloseIncident: true,
},
},
});
@@ -42,6 +43,7 @@ describe('Alert integration settings form', () => {
create_issue: wrapper.vm.createIssueEnabled,
issue_template_key: wrapper.vm.issueTemplate,
send_email: wrapper.vm.sendEmailEnabled,
+ auto_close_incident: wrapper.vm.autoCloseIncident,
}),
);
});
diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
new file mode 100644
index 00000000000..38bcb1e0aab
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
@@ -0,0 +1,76 @@
+import { mount } from '@vue/test-utils';
+import { GlFormCheckbox } from '@gitlab/ui';
+import { createStore } from '~/integrations/edit/store';
+
+import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
+
+describe('ActiveCheckbox', () => {
+ let wrapper;
+
+ const createComponent = (customStateProps = {}, isInheriting = false) => {
+ wrapper = mount(ActiveCheckbox, {
+ store: createStore({
+ customState: { ...customStateProps },
+ }),
+ computed: {
+ isInheriting: () => isInheriting,
+ },
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findGlFormCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findInputInCheckbox = () => findGlFormCheckbox().find('input');
+
+ describe('template', () => {
+ describe('is inheriting adminSettings', () => {
+ it('renders GlFormCheckbox as disabled', () => {
+ createComponent({}, true);
+
+ expect(findGlFormCheckbox().exists()).toBe(true);
+ expect(findInputInCheckbox().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('initialActivated is false', () => {
+ it('renders GlFormCheckbox as unchecked', () => {
+ createComponent({
+ initialActivated: false,
+ });
+
+ expect(findGlFormCheckbox().exists()).toBe(true);
+ expect(findGlFormCheckbox().vm.$attrs.checked).toBe(false);
+ expect(findInputInCheckbox().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ describe('initialActivated is true', () => {
+ beforeEach(() => {
+ createComponent({
+ initialActivated: true,
+ });
+ });
+
+ it('renders GlFormCheckbox as checked', () => {
+ expect(findGlFormCheckbox().exists()).toBe(true);
+ expect(findGlFormCheckbox().vm.$attrs.checked).toBe(true);
+ });
+
+ describe('on checkbox click', () => {
+ it('switches the form value', async () => {
+ findInputInCheckbox().trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlFormCheckbox().vm.$attrs.checked).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/active_toggle_spec.js b/spec/frontend/integrations/edit/components/active_toggle_spec.js
deleted file mode 100644
index 228d8f5fc30..00000000000
--- a/spec/frontend/integrations/edit/components/active_toggle_spec.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { GlToggle } from '@gitlab/ui';
-
-import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
-
-const GL_TOGGLE_ACTIVE_CLASS = 'is-checked';
-const GL_TOGGLE_DISABLED_CLASS = 'is-disabled';
-
-describe('ActiveToggle', () => {
- let wrapper;
-
- const defaultProps = {
- initialActivated: true,
- };
-
- const createComponent = (props = {}, isInheriting = false) => {
- wrapper = mount(ActiveToggle, {
- propsData: { ...defaultProps, ...props },
- computed: {
- isInheriting: () => isInheriting,
- },
- });
- };
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
- const findGlToggle = () => wrapper.find(GlToggle);
- const findButtonInToggle = () => findGlToggle().find('button');
- const findInputInToggle = () => findGlToggle().find('input');
-
- describe('template', () => {
- describe('is inheriting adminSettings', () => {
- it('renders GlToggle as disabled', () => {
- createComponent({}, true);
-
- expect(findGlToggle().exists()).toBe(true);
- expect(findButtonInToggle().classes()).toContain(GL_TOGGLE_DISABLED_CLASS);
- });
- });
-
- describe('initialActivated is false', () => {
- it('renders GlToggle as inactive', () => {
- createComponent({
- initialActivated: false,
- });
-
- expect(findGlToggle().exists()).toBe(true);
- expect(findButtonInToggle().classes()).not.toContain(GL_TOGGLE_ACTIVE_CLASS);
- expect(findInputInToggle().attributes('value')).toBe('false');
- });
- });
-
- describe('initialActivated is true', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders GlToggle as active', () => {
- expect(findGlToggle().exists()).toBe(true);
- expect(findButtonInToggle().classes()).toContain(GL_TOGGLE_ACTIVE_CLASS);
- expect(findInputInToggle().attributes('value')).toBe('true');
- });
-
- describe('on toggle click', () => {
- it('switches the form value', () => {
- findButtonInToggle().trigger('click');
-
- wrapper.vm.$nextTick(() => {
- expect(findButtonInToggle().classes()).not.toContain(GL_TOGGLE_ACTIVE_CLASS);
- expect(findInputInToggle().attributes('value')).toBe('false');
- });
- });
- });
- });
- });
-});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index f8e2eb5e7f4..eeb5d21d62c 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -3,7 +3,7 @@ import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
import { createStore } from '~/integrations/edit/store';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
-import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
+import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
@@ -21,7 +21,7 @@ describe('IntegrationForm', () => {
}),
stubs: {
OverrideDropdown,
- ActiveToggle,
+ ActiveCheckbox,
JiraTriggerFields,
TriggerFields,
},
@@ -39,27 +39,27 @@ describe('IntegrationForm', () => {
});
const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
- const findActiveToggle = () => wrapper.find(ActiveToggle);
+ const findActiveCheckbox = () => wrapper.find(ActiveCheckbox);
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
const findTriggerFields = () => wrapper.find(TriggerFields);
describe('template', () => {
describe('showActive is true', () => {
- it('renders ActiveToggle', () => {
+ it('renders ActiveCheckbox', () => {
createComponent();
- expect(findActiveToggle().exists()).toBe(true);
+ expect(findActiveCheckbox().exists()).toBe(true);
});
});
describe('showActive is false', () => {
- it('does not render ActiveToggle', () => {
+ it('does not render ActiveCheckbox', () => {
createComponent({
showActive: false,
});
- expect(findActiveToggle().exists()).toBe(false);
+ expect(findActiveCheckbox().exists()).toBe(false);
});
});
@@ -137,13 +137,13 @@ describe('IntegrationForm', () => {
});
});
- describe('adminState state is null', () => {
+ describe('defaultState state is null', () => {
it('does not render OverrideDropdown', () => {
createComponent(
{},
{},
{
- adminState: null,
+ defaultState: null,
},
);
@@ -151,13 +151,13 @@ describe('IntegrationForm', () => {
});
});
- describe('adminState state is an object', () => {
+ describe('defaultState state is an object', () => {
it('renders OverrideDropdown', () => {
createComponent(
{},
{},
{
- adminState: {
+ defaultState: {
...mockIntegrationProps,
},
},
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index f58825f6297..a727bb9c734 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -57,7 +57,7 @@ describe('JiraIssuesFields', () => {
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
// browsers don't include unchecked boxes in form submissions.
it('includes issues_enabled as false even if unchecked', () => {
- expect(wrapper.contains('input[name="service[issues_enabled]"]')).toBe(true);
+ expect(wrapper.find('input[name="service[issues_enabled]"]').exists()).toBe(true);
});
it('disables project_key input', () => {
@@ -90,7 +90,23 @@ describe('JiraIssuesFields', () => {
it('contains link to editProjectPath', () => {
createComponent();
- expect(wrapper.contains(`a[href="${defaultProps.editProjectPath}"]`)).toBe(true);
+ expect(wrapper.find(`a[href="${defaultProps.editProjectPath}"]`).exists()).toBe(true);
+ });
+
+ describe('GitLab issues warning', () => {
+ const expectedText = 'Consider disabling GitLab issues';
+
+ it('contains warning when GitLab issues is enabled', () => {
+ createComponent();
+
+ expect(wrapper.text()).toContain(expectedText);
+ });
+
+ it('does not contain warning when GitLab issues is disabled', () => {
+ createComponent({ gitlabIssuesEnabled: false });
+
+ expect(wrapper.text()).not.toContain(expectedText);
+ });
});
});
});
diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
new file mode 100644
index 00000000000..f312c456d5f
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlLink } from '@gitlab/ui';
+import { createStore } from '~/integrations/edit/store';
+
+import { integrationLevels, overrideDropdownDescriptions } from '~/integrations/edit/constants';
+import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
+
+describe('OverrideDropdown', () => {
+ let wrapper;
+
+ const defaultProps = {
+ inheritFromId: 1,
+ override: true,
+ };
+
+ const defaultDefaultStateProps = {
+ integrationLevel: 'group',
+ };
+
+ const createComponent = (props = {}, defaultStateProps = {}) => {
+ wrapper = shallowMount(OverrideDropdown, {
+ propsData: { ...defaultProps, ...props },
+ store: createStore({
+ defaultState: { ...defaultDefaultStateProps, ...defaultStateProps },
+ }),
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findGlLink = () => wrapper.find(GlLink);
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+
+ describe('template', () => {
+ describe('override prop is true', () => {
+ it('renders GlToggle as disabled', () => {
+ createComponent();
+
+ expect(findGlDropdown().props('text')).toBe('Use custom settings');
+ });
+ });
+
+ describe('override prop is false', () => {
+ it('renders GlToggle as disabled', () => {
+ createComponent({ override: false });
+
+ expect(findGlDropdown().props('text')).toBe('Use default settings');
+ });
+ });
+
+ describe('integrationLevel is "project"', () => {
+ it('renders copy mentioning instance (as default fallback)', () => {
+ createComponent(
+ {},
+ {
+ integrationLevel: 'project',
+ },
+ );
+
+ expect(wrapper.text()).toContain(overrideDropdownDescriptions[integrationLevels.INSTANCE]);
+ });
+ });
+
+ describe('integrationLevel is "group"', () => {
+ it('renders copy mentioning group', () => {
+ createComponent(
+ {},
+ {
+ integrationLevel: 'group',
+ },
+ );
+
+ expect(wrapper.text()).toContain(overrideDropdownDescriptions[integrationLevels.GROUP]);
+ });
+ });
+
+ describe('integrationLevel is "instance"', () => {
+ it('renders copy mentioning instance', () => {
+ createComponent(
+ {},
+ {
+ integrationLevel: 'instance',
+ },
+ );
+
+ expect(wrapper.text()).toContain(overrideDropdownDescriptions[integrationLevels.INSTANCE]);
+ });
+ });
+
+ describe('learnMorePath is present', () => {
+ it('renders GlLink with correct link', () => {
+ createComponent({
+ learnMorePath: '/docs',
+ });
+
+ expect(findGlLink().text()).toBe('Learn more');
+ expect(findGlLink().attributes('href')).toBe('/docs');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js
index da2758ec15c..821972b7698 100644
--- a/spec/frontend/integrations/edit/mock_data.js
+++ b/spec/frontend/integrations/edit/mock_data.js
@@ -1,9 +1,6 @@
-// eslint-disable-next-line import/prefer-default-export
export const mockIntegrationProps = {
id: 25,
- activeToggleProps: {
- initialActivated: true,
- },
+ initialActivated: true,
showActive: true,
triggerFieldsProps: {
initialTriggerCommit: false,
diff --git a/spec/frontend/integrations/edit/store/getters_spec.js b/spec/frontend/integrations/edit/store/getters_spec.js
index 700d36edaad..3353e0c84cc 100644
--- a/spec/frontend/integrations/edit/store/getters_spec.js
+++ b/spec/frontend/integrations/edit/store/getters_spec.js
@@ -5,22 +5,22 @@ import { mockIntegrationProps } from '../mock_data';
describe('Integration form store getters', () => {
let state;
const customState = { ...mockIntegrationProps, type: 'CustomState' };
- const adminState = { ...mockIntegrationProps, type: 'AdminState' };
+ const defaultState = { ...mockIntegrationProps, type: 'DefaultState' };
beforeEach(() => {
state = createState({ customState });
});
describe('isInheriting', () => {
- describe('when adminState is null', () => {
+ describe('when defaultState is null', () => {
it('returns false', () => {
expect(isInheriting(state)).toBe(false);
});
});
- describe('when adminState is an object', () => {
+ describe('when defaultState is an object', () => {
beforeEach(() => {
- state.adminState = adminState;
+ state.defaultState = defaultState;
});
describe('when override is false', () => {
@@ -47,11 +47,11 @@ describe('Integration form store getters', () => {
describe('propsSource', () => {
beforeEach(() => {
- state.adminState = adminState;
+ state.defaultState = defaultState;
});
- it('equals adminState if inheriting', () => {
- expect(propsSource(state, { isInheriting: true })).toEqual(adminState);
+ it('equals defaultState if inheriting', () => {
+ expect(propsSource(state, { isInheriting: true })).toEqual(defaultState);
});
it('equals customState if not inheriting', () => {
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
index a8b431aa310..fc193850a94 100644
--- a/spec/frontend/integrations/edit/store/state_spec.js
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -3,8 +3,10 @@ import createState from '~/integrations/edit/store/state';
describe('Integration form state factory', () => {
it('states default to null', () => {
expect(createState()).toEqual({
- adminState: null,
+ defaultState: null,
customState: {},
+ isSaving: false,
+ isTesting: false,
override: false,
});
});
@@ -17,9 +19,9 @@ describe('Integration form state factory', () => {
[null, { inheritFromId: null }, false],
[null, { inheritFromId: 25 }, false],
])(
- 'for adminState: %p, customState: %p: override = `%p`',
- (adminState, customState, expected) => {
- expect(createState({ adminState, customState }).override).toEqual(expected);
+ 'for defaultState: %p, customState: %p: override = `%p`',
+ (defaultState, customState, expected) => {
+ expect(createState({ defaultState, customState }).override).toEqual(expected);
},
);
});
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
index c117a37ff2f..bba851ad796 100644
--- a/spec/frontend/integrations/integration_settings_form_spec.js
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -1,7 +1,9 @@
-import $ from 'jquery';
import MockAdaptor from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+import toast from '~/vue_shared/plugins/global_toast';
+
+jest.mock('~/vue_shared/plugins/global_toast');
describe('IntegrationSettingsForm', () => {
const FIXTURE = 'services/edit_service.html';
@@ -11,7 +13,7 @@ describe('IntegrationSettingsForm', () => {
loadFixtures(FIXTURE);
});
- describe('contructor', () => {
+ describe('constructor', () => {
let integrationSettingsForm;
beforeEach(() => {
@@ -24,16 +26,10 @@ describe('IntegrationSettingsForm', () => {
expect(integrationSettingsForm.$form).toBeDefined();
expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
expect(integrationSettingsForm.formActive).toBeDefined();
-
- // Form Child Elements
- expect(integrationSettingsForm.$submitBtn).toBeDefined();
- expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
- expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
});
it('should initialize form metadata on class object', () => {
expect(integrationSettingsForm.testEndPoint).toBeDefined();
- expect(integrationSettingsForm.canTestService).toBeDefined();
});
});
@@ -59,69 +55,6 @@ describe('IntegrationSettingsForm', () => {
});
});
- describe('toggleSubmitBtnLabel', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- });
-
- it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
- integrationSettingsForm.canTestService = true;
- integrationSettingsForm.formActive = true;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual(
- 'Test settings and save changes',
- );
- });
-
- it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
- integrationSettingsForm.canTestService = false;
- integrationSettingsForm.formActive = false;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
-
- integrationSettingsForm.formActive = true;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
-
- integrationSettingsForm.canTestService = true;
- integrationSettingsForm.formActive = false;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
- });
- });
-
- describe('toggleSubmitBtnState', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- });
-
- it('should disable Save button and show loader animation when called with `true`', () => {
- integrationSettingsForm.toggleSubmitBtnState(true);
-
- expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy();
- expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy();
- });
-
- it('should enable Save button and hide loader animation when called with `false`', () => {
- integrationSettingsForm.toggleSubmitBtnState(false);
-
- expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy();
- expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy();
- });
- });
-
describe('testSettings', () => {
let integrationSettingsForm;
let formData;
@@ -133,6 +66,8 @@ describe('IntegrationSettingsForm', () => {
jest.spyOn(axios, 'put');
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
+
// eslint-disable-next-line no-jquery/no-serialize
formData = integrationSettingsForm.$form.serialize();
});
@@ -141,128 +76,60 @@ describe('IntegrationSettingsForm', () => {
mock.restore();
});
- it('should make an ajax request with provided `formData`', () => {
- return integrationSettingsForm.testSettings(formData).then(() => {
- expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
- });
- });
-
- it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => {
- const errorMessage = 'Test failed.';
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: errorMessage,
- service_response: 'some error',
- test_failed: true,
- });
+ it('should make an ajax request with provided `formData`', async () => {
+ await integrationSettingsForm.testSettings(formData);
- return integrationSettingsForm.testSettings(formData).then(() => {
- const $flashContainer = $('.flash-container');
-
- expect(
- $flashContainer
- .find('.flash-text')
- .text()
- .trim(),
- ).toEqual('Test failed. some error');
-
- expect($flashContainer.find('.flash-action')).toBeDefined();
- expect(
- $flashContainer
- .find('.flash-action')
- .text()
- .trim(),
- ).toEqual('Save anyway');
- });
+ expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
});
- it('should not show error Flash with `Save anyway` action if ajax request responds with error in validation', () => {
- const errorMessage = 'Validations failed.';
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: errorMessage,
- service_response: 'some error',
- test_failed: false,
- });
-
- return integrationSettingsForm.testSettings(formData).then(() => {
- const $flashContainer = $('.flash-container');
-
- expect(
- $flashContainer
- .find('.flash-text')
- .text()
- .trim(),
- ).toEqual('Validations failed. some error');
-
- expect($flashContainer.find('.flash-action')).toBeDefined();
- expect(
- $flashContainer
- .find('.flash-action')
- .text()
- .trim(),
- ).toEqual('');
- });
- });
-
- it('should submit form if ajax request responds without any error in test', () => {
+ it('should show success message if test is successful', async () => {
jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: false,
});
- return integrationSettingsForm.testSettings(formData).then(() => {
- expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
- });
- });
+ await integrationSettingsForm.testSettings(formData);
- it('should submit form when clicked on `Save anyway` action of error Flash', () => {
- jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
+ expect(toast).toHaveBeenCalledWith('Connection successful.');
+ });
+ it('should show error message if ajax request responds with test error', async () => {
const errorMessage = 'Test failed.';
+ const serviceResponse = 'some error';
+
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: errorMessage,
- test_failed: true,
+ service_response: serviceResponse,
+ test_failed: false,
});
- return integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- const $flashAction = $('.flash-container .flash-action');
+ await integrationSettingsForm.testSettings(formData);
- expect($flashAction).toBeDefined();
-
- $flashAction.get(0).click();
- })
- .then(() => {
- expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
- });
+ expect(toast).toHaveBeenCalledWith(`${errorMessage} ${serviceResponse}`);
});
- it('should show error Flash if ajax request failed', () => {
+ it('should show error message if ajax request failed', async () => {
const errorMessage = 'Something went wrong on our end.';
mock.onPut(integrationSettingsForm.testEndPoint).networkError();
- return integrationSettingsForm.testSettings(formData).then(() => {
- expect(
- $('.flash-container .flash-text')
- .text()
- .trim(),
- ).toEqual(errorMessage);
- });
+ await integrationSettingsForm.testSettings(formData);
+
+ expect(toast).toHaveBeenCalledWith(errorMessage);
});
- it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
+ it('should always dispatch `setIsTesting` with `false` once request is completed', async () => {
+ const dispatchSpy = jest.fn();
+
mock.onPut(integrationSettingsForm.testEndPoint).networkError();
- jest.spyOn(integrationSettingsForm, 'toggleSubmitBtnState').mockImplementation(() => {});
+ integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
- return integrationSettingsForm.testSettings(formData).then(() => {
- expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
- });
+ await integrationSettingsForm.testSettings(formData);
+
+ expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
});
});
});
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
new file mode 100644
index 00000000000..bfbe4ec8e70
--- /dev/null
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -0,0 +1,289 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
+import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
+
+const issuable1 = {
+ id: 200,
+ reference: 'foo/bar#123',
+ displayReference: '#123',
+ title: 'some title',
+ path: '/foo/bar/issues/123',
+ state: 'opened',
+};
+
+const issuable2 = {
+ id: 201,
+ reference: 'foo/bar#124',
+ displayReference: '#124',
+ title: 'some other thing',
+ path: '/foo/bar/issues/124',
+ state: 'opened',
+};
+
+const pathIdSeparator = PathIdSeparator.Issue;
+
+const findFormInput = wrapper => wrapper.find('.js-add-issuable-form-input').element;
+
+const findRadioInput = (inputs, value) => inputs.filter(input => input.element.value === value)[0];
+
+const findRadioInputs = wrapper => wrapper.findAll('[name="linked-issue-type-radio"]');
+
+const constructWrapper = props => {
+ return shallowMount(AddIssuableForm, {
+ propsData: {
+ inputValue: '',
+ pendingReferences: [],
+ pathIdSeparator,
+ ...props,
+ },
+ });
+};
+
+describe('AddIssuableForm', () => {
+ let wrapper;
+
+ afterEach(() => {
+ // Jest doesn't blur an item even if it is destroyed,
+ // so blur the input manually after each test
+ const input = findFormInput(wrapper);
+ if (input) input.blur();
+
+ wrapper.destroy();
+ });
+
+ describe('with data', () => {
+ describe('without references', () => {
+ describe('without any input text', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(AddIssuableForm, {
+ propsData: {
+ inputValue: '',
+ pendingReferences: [],
+ pathIdSeparator,
+ },
+ });
+ });
+
+ it('should have disabled submit button', () => {
+ expect(wrapper.vm.$refs.addButton.disabled).toBe(true);
+ expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
+ });
+ });
+
+ describe('with input text', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(AddIssuableForm, {
+ propsData: {
+ inputValue: 'foo',
+ pendingReferences: [],
+ pathIdSeparator,
+ },
+ });
+ });
+
+ it('should not have disabled submit button', () => {
+ expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
+ });
+ });
+ });
+
+ describe('with references', () => {
+ const inputValue = 'foo #123';
+
+ beforeEach(() => {
+ wrapper = mount(AddIssuableForm, {
+ propsData: {
+ inputValue,
+ pendingReferences: [issuable1.reference, issuable2.reference],
+ pathIdSeparator,
+ },
+ });
+ });
+
+ it('should put input value in place', () => {
+ expect(findFormInput(wrapper).value).toEqual(inputValue);
+ });
+
+ it('should render pending issuables items', () => {
+ expect(wrapper.findAll('.js-add-issuable-form-token-list-item').length).toEqual(2);
+ });
+
+ it('should not have disabled submit button', () => {
+ expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
+ });
+ });
+
+ describe('when issuable type is "issue"', () => {
+ beforeEach(() => {
+ wrapper = mount(AddIssuableForm, {
+ propsData: {
+ inputValue: '',
+ issuableType: issuableTypesMap.ISSUE,
+ pathIdSeparator,
+ pendingReferences: [],
+ },
+ });
+ });
+
+ it('does not show radio inputs', () => {
+ expect(findRadioInputs(wrapper).length).toBe(0);
+ });
+ });
+
+ describe('when issuable type is "epic"', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(AddIssuableForm, {
+ propsData: {
+ inputValue: '',
+ issuableType: issuableTypesMap.EPIC,
+ pathIdSeparator,
+ pendingReferences: [],
+ },
+ });
+ });
+
+ it('does not show radio inputs', () => {
+ expect(findRadioInputs(wrapper).length).toBe(0);
+ });
+ });
+
+ describe('when it is a Linked Issues form', () => {
+ beforeEach(() => {
+ wrapper = mount(AddIssuableForm, {
+ propsData: {
+ inputValue: '',
+ showCategorizedIssues: true,
+ issuableType: issuableTypesMap.ISSUE,
+ pathIdSeparator,
+ pendingReferences: [],
+ },
+ });
+ });
+
+ it('shows radio inputs to allow categorisation of blocking issues', () => {
+ expect(findRadioInputs(wrapper).length).toBeGreaterThan(0);
+ });
+
+ describe('form radio buttons', () => {
+ let radioInputs;
+
+ beforeEach(() => {
+ radioInputs = findRadioInputs(wrapper);
+ });
+
+ it('shows "relates to" option', () => {
+ expect(findRadioInput(radioInputs, linkedIssueTypesMap.RELATES_TO)).not.toBeNull();
+ });
+
+ it('shows "blocks" option', () => {
+ expect(findRadioInput(radioInputs, linkedIssueTypesMap.BLOCKS)).not.toBeNull();
+ });
+
+ it('shows "is blocked by" option', () => {
+ expect(findRadioInput(radioInputs, linkedIssueTypesMap.IS_BLOCKED_BY)).not.toBeNull();
+ });
+
+ it('shows 3 options in total', () => {
+ expect(radioInputs.length).toBe(3);
+ });
+ });
+
+ describe('when the form is submitted', () => {
+ it('emits an event with a "relates_to" link type when the "relates to" radio input selected', done => {
+ jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
+
+ wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO;
+ wrapper.vm.onFormSubmit();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.RELATES_TO,
+ });
+ done();
+ });
+ });
+
+ it('emits an event with a "blocks" link type when the "blocks" radio input selected', done => {
+ jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
+
+ wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS;
+ wrapper.vm.onFormSubmit();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.BLOCKS,
+ });
+ done();
+ });
+ });
+
+ it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', done => {
+ jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
+
+ wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY;
+ wrapper.vm.onFormSubmit();
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
+ });
+ done();
+ });
+ });
+
+ it('shows error message when error is present', done => {
+ const itemAddFailureMessage = 'Something went wrong while submitting.';
+ wrapper.setProps({
+ hasError: true,
+ itemAddFailureMessage,
+ });
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.gl-field-error').exists()).toBe(true);
+ expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('transformedAutocompleteSources', () => {
+ const autoCompleteSources = {
+ issues: 'http://localhost/autocomplete/issues',
+ epics: 'http://localhost/autocomplete/epics',
+ };
+
+ it('returns autocomplete object', () => {
+ wrapper = constructWrapper({
+ autoCompleteSources,
+ });
+
+ expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources);
+
+ wrapper = constructWrapper({
+ autoCompleteSources,
+ confidential: false,
+ });
+
+ expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources);
+ });
+
+ it('returns autocomplete sources with query `confidential_only`, when it is confidential', () => {
+ wrapper = constructWrapper({
+ autoCompleteSources,
+ confidential: true,
+ });
+
+ const actualSources = wrapper.vm.transformedAutocompleteSources;
+
+ expect(actualSources.epics).toContain('?confidential_only=true');
+ expect(actualSources.issues).toContain('?confidential_only=true');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable/related_issues/components/issue_token_spec.js b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
new file mode 100644
index 00000000000..553721fa783
--- /dev/null
+++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
@@ -0,0 +1,241 @@
+import Vue from 'vue';
+import { PathIdSeparator } from '~/related_issues/constants';
+import issueToken from '~/related_issues/components/issue_token.vue';
+
+describe('IssueToken', () => {
+ const idKey = 200;
+ const displayReference = 'foo/bar#123';
+ const title = 'some title';
+ const pathIdSeparator = PathIdSeparator.Issue;
+ const eventNamespace = 'pendingIssuable';
+ let IssueToken;
+ let vm;
+
+ beforeEach(() => {
+ IssueToken = Vue.extend(issueToken);
+ });
+
+ afterEach(() => {
+ if (vm) {
+ vm.$destroy();
+ }
+ });
+
+ describe('with reference supplied', () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ },
+ }).$mount();
+ });
+
+ it('shows reference', () => {
+ expect(vm.$el.textContent.trim()).toEqual(displayReference);
+ });
+
+ it('does not link without path specified', () => {
+ expect(vm.$refs.link.tagName.toLowerCase()).toEqual('span');
+ expect(vm.$refs.link.getAttribute('href')).toBeNull();
+ });
+ });
+
+ describe('with reference and title supplied', () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ title,
+ },
+ }).$mount();
+ });
+
+ it('shows reference and title', () => {
+ expect(vm.$refs.reference.textContent.trim()).toEqual(displayReference);
+ expect(vm.$refs.title.textContent.trim()).toEqual(title);
+ });
+ });
+
+ describe('with path supplied', () => {
+ const path = '/foo/bar/issues/123';
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ title,
+ path,
+ },
+ }).$mount();
+ });
+
+ it('links reference and title', () => {
+ expect(vm.$refs.link.getAttribute('href')).toEqual(path);
+ });
+ });
+
+ describe('with state supplied', () => {
+ describe("`state: 'opened'`", () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ state: 'opened',
+ },
+ }).$mount();
+ });
+
+ it('shows green circle icon', () => {
+ expect(vm.$el.querySelector('.issue-token-state-icon-open.fa.fa-circle-o')).toBeDefined();
+ });
+ });
+
+ describe("`state: 'reopened'`", () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ state: 'reopened',
+ },
+ }).$mount();
+ });
+
+ it('shows green circle icon', () => {
+ expect(vm.$el.querySelector('.issue-token-state-icon-open.fa.fa-circle-o')).toBeDefined();
+ });
+ });
+
+ describe("`state: 'closed'`", () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ state: 'closed',
+ },
+ }).$mount();
+ });
+
+ it('shows red minus icon', () => {
+ expect(vm.$el.querySelector('.issue-token-state-icon-closed.fa.fa-minus')).toBeDefined();
+ });
+ });
+ });
+
+ describe('with reference, title, state', () => {
+ const state = 'opened';
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ title,
+ state,
+ },
+ }).$mount();
+ });
+
+ it('shows reference, title, and state', () => {
+ const stateIcon = vm.$refs.reference.querySelector('svg');
+
+ expect(stateIcon.getAttribute('aria-label')).toEqual(state);
+ expect(vm.$refs.reference.textContent.trim()).toEqual(displayReference);
+ expect(vm.$refs.title.textContent.trim()).toEqual(title);
+ });
+ });
+
+ describe('with canRemove', () => {
+ describe('`canRemove: false` (default)', () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ },
+ }).$mount();
+ });
+
+ it('does not have remove button', () => {
+ expect(vm.$el.querySelector('.issue-token-remove-button')).toBeNull();
+ });
+ });
+
+ describe('`canRemove: true`', () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ canRemove: true,
+ },
+ }).$mount();
+ });
+
+ it('has remove button', () => {
+ expect(vm.$el.querySelector('.issue-token-remove-button')).toBeDefined();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ },
+ }).$mount();
+ });
+
+ it('when getting checked', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.onRemoveRequest();
+
+ expect(vm.$emit).toHaveBeenCalledWith('pendingIssuableRemoveRequest', vm.idKey);
+ });
+ });
+
+ describe('tooltip', () => {
+ beforeEach(() => {
+ vm = new IssueToken({
+ propsData: {
+ idKey,
+ eventNamespace,
+ displayReference,
+ pathIdSeparator,
+ canRemove: true,
+ },
+ }).$mount();
+ });
+
+ it('should not be escaped', () => {
+ const { originalTitle } = vm.$refs.removeButton.dataset;
+
+ expect(originalTitle).toEqual(`Remove ${displayReference}`);
+ });
+ });
+});
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
new file mode 100644
index 00000000000..0f88e4d71fe
--- /dev/null
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -0,0 +1,206 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlButton, GlIcon } from '@gitlab/ui';
+import {
+ issuable1,
+ issuable2,
+ issuable3,
+} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
+import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
+import {
+ linkedIssueTypesMap,
+ linkedIssueTypesTextMap,
+ PathIdSeparator,
+} from '~/related_issues/constants';
+
+describe('RelatedIssuesBlock', () => {
+ let wrapper;
+
+ const findIssueCountBadgeAddButton = () => wrapper.find(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with defaults', () => {
+ beforeEach(() => {
+ wrapper = mount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'issue',
+ },
+ });
+ });
+
+ it('displays "Linked issues" in the header', () => {
+ expect(wrapper.find('.card-title').text()).toContain('Linked issues');
+ });
+
+ it('unable to add new related issues', () => {
+ expect(findIssueCountBadgeAddButton().exists()).toBe(false);
+ });
+
+ it('add related issues form is hidden', () => {
+ expect(wrapper.find('.js-add-related-issues-form-area').exists()).toBe(false);
+ });
+ });
+
+ describe('with headerText slot', () => {
+ it('displays header text slot data', () => {
+ const headerText = '<div>custom header text</div>';
+
+ wrapper = shallowMount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'issue',
+ },
+ slots: { headerText },
+ });
+
+ expect(wrapper.find('.card-title').html()).toContain(headerText);
+ });
+ });
+
+ describe('with headerActions slot', () => {
+ it('displays header actions slot data', () => {
+ const headerActions = '<button data-testid="custom-button">custom button</button>';
+
+ wrapper = shallowMount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'issue',
+ },
+ slots: { headerActions },
+ });
+
+ expect(wrapper.find('[data-testid="custom-button"]').html()).toBe(headerActions);
+ });
+ });
+
+ describe('with isFetching=true', () => {
+ beforeEach(() => {
+ wrapper = mount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ isFetching: true,
+ issuableType: 'issue',
+ },
+ });
+ });
+
+ it('should show `...` badge count', () => {
+ expect(wrapper.vm.badgeLabel).toBe('...');
+ });
+ });
+
+ describe('with canAddRelatedIssues=true', () => {
+ beforeEach(() => {
+ wrapper = mount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ canAdmin: true,
+ issuableType: 'issue',
+ },
+ });
+ });
+
+ it('can add new related issues', () => {
+ expect(findIssueCountBadgeAddButton().exists()).toBe(true);
+ });
+ });
+
+ describe('with isFormVisible=true', () => {
+ beforeEach(() => {
+ wrapper = mount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ isFormVisible: true,
+ issuableType: 'issue',
+ },
+ });
+ });
+
+ it('shows add related issues form', () => {
+ expect(wrapper.find('.js-add-related-issues-form-area').exists()).toBe(true);
+ });
+ });
+
+ describe('showCategorizedIssues prop', () => {
+ const issueList = () => wrapper.findAll('.js-related-issues-token-list-item');
+ const categorizedHeadings = () => wrapper.findAll('h4');
+ const headingTextAt = index =>
+ categorizedHeadings()
+ .at(index)
+ .text();
+ const mountComponent = showCategorizedIssues => {
+ wrapper = mount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ relatedIssues: [issuable1, issuable2, issuable3],
+ issuableType: 'issue',
+ showCategorizedIssues,
+ },
+ });
+ };
+
+ describe('when showCategorizedIssues=true', () => {
+ beforeEach(() => mountComponent(true));
+
+ it('should render issue tokens items', () => {
+ expect(issueList()).toHaveLength(3);
+ });
+
+ it('shows "Blocks" heading', () => {
+ const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS];
+
+ expect(headingTextAt(0)).toBe(blocks);
+ });
+
+ it('shows "Is blocked by" heading', () => {
+ const isBlockedBy = linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY];
+
+ expect(headingTextAt(1)).toBe(isBlockedBy);
+ });
+
+ it('shows "Relates to" heading', () => {
+ const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO];
+
+ expect(headingTextAt(2)).toBe(relatesTo);
+ });
+ });
+
+ describe('when showCategorizedIssues=false', () => {
+ it('should render issues as a flat list with no header', () => {
+ mountComponent(false);
+
+ expect(issueList()).toHaveLength(3);
+ expect(categorizedHeadings()).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('renders correct icon when', () => {
+ [
+ {
+ icon: 'issues',
+ issuableType: 'issue',
+ },
+ {
+ icon: 'epic',
+ issuableType: 'epic',
+ },
+ ].forEach(({ issuableType, icon }) => {
+ it(`issuableType=${issuableType} is passed`, () => {
+ wrapper = shallowMount(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType,
+ },
+ });
+
+ const iconComponent = wrapper.find(GlIcon);
+ expect(iconComponent.exists()).toBe(true);
+ expect(iconComponent.props('name')).toBe(icon);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
new file mode 100644
index 00000000000..6cf0b9d21ea
--- /dev/null
+++ b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
@@ -0,0 +1,190 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import {
+ issuable1,
+ issuable2,
+ issuable3,
+ issuable4,
+ issuable5,
+} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import RelatedIssuesList from '~/related_issues/components/related_issues_list.vue';
+import { PathIdSeparator } from '~/related_issues/constants';
+
+describe('RelatedIssuesList', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with defaults', () => {
+ const heading = 'Related to';
+
+ beforeEach(() => {
+ wrapper = shallowMount(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'issue',
+ heading,
+ },
+ });
+ });
+
+ it('shows a heading', () => {
+ expect(wrapper.find('h4').text()).toContain(heading);
+ });
+
+ it('should not show loading icon', () => {
+ expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
+ });
+ });
+
+ describe('with isFetching=true', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ isFetching: true,
+ issuableType: 'issue',
+ },
+ });
+ });
+
+ it('should show loading icon', () => {
+ expect(wrapper.vm.$refs.loadingIcon).toBeDefined();
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
+ issuableType: 'issue',
+ },
+ });
+ });
+
+ it('updates the order correctly when an item is moved to the top', () => {
+ const beforeAfterIds = wrapper.vm.getBeforeAfterId(
+ wrapper.vm.$el.querySelector('ul li:first-child'),
+ );
+
+ expect(beforeAfterIds.beforeId).toBeNull();
+ expect(beforeAfterIds.afterId).toBe(2);
+ });
+
+ it('updates the order correctly when an item is moved to the bottom', () => {
+ const beforeAfterIds = wrapper.vm.getBeforeAfterId(
+ wrapper.vm.$el.querySelector('ul li:last-child'),
+ );
+
+ expect(beforeAfterIds.beforeId).toBe(4);
+ expect(beforeAfterIds.afterId).toBeNull();
+ });
+
+ it('updates the order correctly when an item is swapped with adjacent item', () => {
+ const beforeAfterIds = wrapper.vm.getBeforeAfterId(
+ wrapper.vm.$el.querySelector('ul li:nth-child(3)'),
+ );
+
+ expect(beforeAfterIds.beforeId).toBe(2);
+ expect(beforeAfterIds.afterId).toBe(4);
+ });
+
+ it('updates the order correctly when an item is moved somewhere in the middle', () => {
+ const beforeAfterIds = wrapper.vm.getBeforeAfterId(
+ wrapper.vm.$el.querySelector('ul li:nth-child(4)'),
+ );
+
+ expect(beforeAfterIds.beforeId).toBe(3);
+ expect(beforeAfterIds.afterId).toBe(5);
+ });
+ });
+
+ describe('issuableOrderingId returns correct issuable order id when', () => {
+ it('issuableType is epic', () => {
+ wrapper = shallowMount(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'issue',
+ },
+ });
+
+ expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.epicIssueId);
+ });
+
+ it('issuableType is issue', () => {
+ wrapper = shallowMount(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'epic',
+ },
+ });
+
+ expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.id);
+ });
+ });
+
+ describe('renders correct ordering id when', () => {
+ let relatedIssues;
+
+ beforeAll(() => {
+ relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5];
+ });
+
+ it('issuableType is epic', () => {
+ wrapper = shallowMount(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'epic',
+ relatedIssues,
+ },
+ });
+
+ const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
+
+ Array.from(listItems).forEach((item, index) => {
+ expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].id);
+ });
+ });
+
+ it('issuableType is issue', () => {
+ wrapper = shallowMount(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: 'issue',
+ relatedIssues,
+ },
+ });
+
+ const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
+
+ Array.from(listItems).forEach((item, index) => {
+ expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].epicIssueId);
+ });
+ });
+ });
+
+ describe('related item contents', () => {
+ beforeAll(() => {
+ wrapper = mount(RelatedIssuesList, {
+ propsData: {
+ issuableType: 'issue',
+ pathIdSeparator: PathIdSeparator.Issue,
+ relatedIssues: [issuable1],
+ },
+ });
+ });
+
+ it('shows due date', () => {
+ expect(
+ wrapper
+ .find(IssueDueDate)
+ .find('.board-card-info-text')
+ .text(),
+ ).toBe('Nov 22, 2010');
+ });
+ });
+});
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
new file mode 100644
index 00000000000..2544d0bd030
--- /dev/null
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -0,0 +1,341 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ defaultProps,
+ issuable1,
+ issuable2,
+} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
+import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
+import relatedIssuesService from '~/related_issues/services/related_issues_service';
+import { linkedIssueTypesMap } from '~/related_issues/constants';
+import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+
+jest.mock('~/flash');
+
+describe('RelatedIssuesRoot', () => {
+ let wrapper;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(defaultProps.endpoint).reply(200, []);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const createComponent = (mountFn = mount) => {
+ wrapper = mountFn(RelatedIssuesRoot, {
+ propsData: defaultProps,
+ });
+
+ // 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(() => {
+ wrapper.vm.store.setRelatedIssues([issuable1]);
+ });
+ });
+
+ it('remove related issue and succeeds', () => {
+ mock.onDelete(issuable1.referencePath).reply(200, { issues: [] });
+
+ wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id);
+
+ return axios.waitForAll().then(() => {
+ expect(wrapper.vm.state.relatedIssues).toEqual([]);
+ });
+ });
+
+ it('remove related issue, fails, and restores to related issues', () => {
+ mock.onDelete(issuable1.referencePath).reply(422, {});
+
+ wrapper.vm.onRelatedIssueRemoveRequest(issuable1.id);
+
+ return axios.waitForAll().then(() => {
+ expect(wrapper.vm.state.relatedIssues).toHaveLength(1);
+ expect(wrapper.vm.state.relatedIssues[0].id).toEqual(issuable1.id);
+ });
+ });
+ });
+
+ describe('onToggleAddRelatedIssuesForm', () => {
+ beforeEach(() => createComponent(shallowMount));
+
+ it('toggle related issues form to visible', () => {
+ wrapper.vm.onToggleAddRelatedIssuesForm();
+
+ expect(wrapper.vm.isFormVisible).toEqual(true);
+ });
+
+ it('show add related issues form to hidden', () => {
+ wrapper.vm.isFormVisible = true;
+
+ wrapper.vm.onToggleAddRelatedIssuesForm();
+
+ expect(wrapper.vm.isFormVisible).toEqual(false);
+ });
+ });
+
+ describe('onPendingIssueRemoveRequest', () => {
+ beforeEach(() =>
+ createComponent().then(() => {
+ wrapper.vm.store.setPendingReferences([issuable1.reference]);
+ }),
+ );
+
+ it('remove pending related issue', () => {
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
+
+ wrapper.vm.onPendingIssueRemoveRequest(0);
+
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
+ });
+ });
+
+ describe('onPendingFormSubmit', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
+ .mockReturnValue(Promise.reject());
+
+ return createComponent().then(() => {
+ jest.spyOn(wrapper.vm, 'processAllReferences');
+ jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
+ createFlash.mockClear();
+ });
+ });
+
+ it('processes references before submitting', () => {
+ const input = '#123';
+ const linkedIssueType = linkedIssueTypesMap.RELATES_TO;
+ const emitObj = {
+ pendingReferences: input,
+ linkedIssueType,
+ };
+
+ wrapper.vm.onPendingFormSubmit(emitObj);
+
+ expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
+ expect(wrapper.vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType);
+ });
+
+ it('submit zero pending issue 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);
+ });
+ });
+
+ it('submit pending issue as related issue', () => {
+ mock.onPost(defaultProps.endpoint).reply(200, {
+ issuables: [issuable1],
+ result: {
+ message: 'something was successfully related',
+ 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);
+ });
+ });
+
+ it('submit multiple pending issues as related issues', () => {
+ mock.onPost(defaultProps.endpoint).reply(200, {
+ issuables: [issuable1, issuable2],
+ result: {
+ message: 'something was successfully related',
+ 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);
+ });
+ });
+
+ it('displays a message from the backend upon error', () => {
+ 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', () => {
+ wrapper.vm.onPendingFormCancel();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.isFormVisible).toEqual(false);
+ expect(wrapper.vm.inputValue).toEqual('');
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('fetchRelatedIssues', () => {
+ beforeEach(() => createComponent());
+
+ it('sets isFetching while fetching', () => {
+ wrapper.vm.fetchRelatedIssues();
+
+ expect(wrapper.vm.isFetching).toEqual(true);
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.isFetching).toEqual(false);
+ });
+ });
+
+ it('should fetch related issues', () => {
+ mock.onGet(defaultProps.endpoint).reply(200, [issuable1, issuable2]);
+
+ wrapper.vm.fetchRelatedIssues();
+
+ return waitForPromises().then(() => {
+ 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);
+ });
+ });
+ });
+
+ describe('onInput', () => {
+ beforeEach(() => createComponent());
+
+ it('fill in issue number reference and adds to pending related issues', () => {
+ const input = '#123 ';
+ wrapper.vm.onInput({
+ untouchedRawReferences: [input.trim()],
+ touchedReference: input,
+ });
+
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
+ expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123');
+ });
+
+ it('fill in with full reference', () => {
+ const input = 'asdf/qwer#444 ';
+ wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input });
+
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
+ expect(wrapper.vm.state.pendingReferences[0]).toEqual('asdf/qwer#444');
+ });
+
+ it('fill in with issue link', () => {
+ const link = 'http://localhost:3000/foo/bar/issues/111';
+ const input = `${link} `;
+ wrapper.vm.onInput({ untouchedRawReferences: [input.trim()], touchedReference: input });
+
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
+ expect(wrapper.vm.state.pendingReferences[0]).toEqual(link);
+ });
+
+ it('fill in with multiple references', () => {
+ const input = 'asdf/qwer#444 #12 ';
+ wrapper.vm.onInput({
+ untouchedRawReferences: input.trim().split(/\s/),
+ touchedReference: 2,
+ });
+
+ 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');
+ });
+
+ it('fill in with some invalid things', () => {
+ const input = 'something random ';
+ wrapper.vm.onInput({
+ untouchedRawReferences: input.trim().split(/\s/),
+ touchedReference: 2,
+ });
+
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(2);
+ expect(wrapper.vm.state.pendingReferences[0]).toEqual('something');
+ expect(wrapper.vm.state.pendingReferences[1]).toEqual('random');
+ });
+ });
+
+ 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('processAllReferences', () => {
+ beforeEach(() => createComponent());
+
+ it('add valid reference to pending', () => {
+ const input = '#123';
+ wrapper.vm.processAllReferences(input);
+
+ expect(wrapper.vm.state.pendingReferences).toHaveLength(1);
+ expect(wrapper.vm.state.pendingReferences[0]).toEqual('#123');
+ });
+
+ 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');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js b/spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js
new file mode 100644
index 00000000000..ada1c44560f
--- /dev/null
+++ b/spec/frontend/issuable/related_issues/stores/related_issues_store_spec.js
@@ -0,0 +1,111 @@
+import {
+ issuable1,
+ issuable2,
+ issuable3,
+ issuable4,
+ issuable5,
+} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
+import RelatedIssuesStore from '~/related_issues/stores/related_issues_store';
+
+describe('RelatedIssuesStore', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new RelatedIssuesStore();
+ });
+
+ describe('setRelatedIssues', () => {
+ it('defaults to empty array', () => {
+ expect(store.state.relatedIssues).toEqual([]);
+ });
+
+ it('sets issues', () => {
+ const relatedIssues = [issuable1];
+ store.setRelatedIssues(relatedIssues);
+
+ expect(store.state.relatedIssues).toEqual(relatedIssues);
+ });
+ });
+
+ describe('addRelatedIssues', () => {
+ it('adds related issues', () => {
+ store.state.relatedIssues = [issuable1];
+ store.addRelatedIssues([issuable2, issuable3]);
+
+ expect(store.state.relatedIssues).toEqual([issuable1, issuable2, issuable3]);
+ });
+ });
+
+ describe('removeRelatedIssue', () => {
+ it('removes issue', () => {
+ store.state.relatedIssues = [issuable1];
+
+ store.removeRelatedIssue(issuable1);
+
+ expect(store.state.relatedIssues).toEqual([]);
+ });
+
+ it('removes issue with multiple in store', () => {
+ store.state.relatedIssues = [issuable1, issuable2];
+
+ store.removeRelatedIssue(issuable1);
+
+ expect(store.state.relatedIssues).toEqual([issuable2]);
+ });
+ });
+
+ describe('updateIssueOrder', () => {
+ it('updates issue order', () => {
+ store.state.relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5];
+
+ expect(store.state.relatedIssues[3].id).toBe(issuable4.id);
+ store.updateIssueOrder(3, 0);
+
+ expect(store.state.relatedIssues[0].id).toBe(issuable4.id);
+ });
+ });
+
+ describe('setPendingReferences', () => {
+ it('defaults to empty array', () => {
+ expect(store.state.pendingReferences).toEqual([]);
+ });
+
+ it('sets pending references', () => {
+ const relatedIssues = [issuable1.reference];
+ store.setPendingReferences(relatedIssues);
+
+ expect(store.state.pendingReferences).toEqual(relatedIssues);
+ });
+ });
+
+ describe('addPendingReferences', () => {
+ it('adds a reference', () => {
+ store.state.pendingReferences = [issuable1.reference];
+ store.addPendingReferences([issuable2.reference, issuable3.reference]);
+
+ expect(store.state.pendingReferences).toEqual([
+ issuable1.reference,
+ issuable2.reference,
+ issuable3.reference,
+ ]);
+ });
+ });
+
+ describe('removePendingRelatedIssue', () => {
+ it('removes issue', () => {
+ store.state.pendingReferences = [issuable1.reference];
+
+ store.removePendingRelatedIssue(0);
+
+ expect(store.state.pendingReferences).toEqual([]);
+ });
+
+ it('removes issue with multiple in store', () => {
+ store.state.pendingReferences = [issuable1.reference, issuable2.reference];
+
+ store.removePendingRelatedIssue(0);
+
+ expect(store.state.pendingReferences).toEqual([issuable2.reference]);
+ });
+ });
+});
diff --git a/spec/frontend/issuable_create/components/issuable_create_root_spec.js b/spec/frontend/issuable_create/components/issuable_create_root_spec.js
new file mode 100644
index 00000000000..675d01ae4af
--- /dev/null
+++ b/spec/frontend/issuable_create/components/issuable_create_root_spec.js
@@ -0,0 +1,64 @@
+import { mount } from '@vue/test-utils';
+
+import IssuableCreateRoot from '~/issuable_create/components/issuable_create_root.vue';
+import IssuableForm from '~/issuable_create/components/issuable_form.vue';
+
+const createComponent = ({
+ descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
+ descriptionHelpPath = '/help/user/markdown',
+ labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json',
+ labelsManagePath = '/gitlab-org/gitlab-shell/-/labels',
+} = {}) => {
+ return mount(IssuableCreateRoot, {
+ propsData: {
+ descriptionPreviewPath,
+ descriptionHelpPath,
+ labelsFetchPath,
+ labelsManagePath,
+ },
+ slots: {
+ title: `
+ <h1 class="js-create-title">New Issuable</h1>
+ `,
+ actions: `
+ <button class="js-issuable-save">Submit issuable</button>
+ `,
+ },
+ });
+};
+
+describe('IssuableCreateRoot', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('renders component container element with class "issuable-create-container"', () => {
+ expect(wrapper.classes()).toContain('issuable-create-container');
+ });
+
+ it('renders contents for slot "title"', () => {
+ const titleEl = wrapper.find('h1.js-create-title');
+
+ expect(titleEl.exists()).toBe(true);
+ expect(titleEl.text()).toBe('New Issuable');
+ });
+
+ it('renders issuable-form component', () => {
+ expect(wrapper.find(IssuableForm).exists()).toBe(true);
+ });
+
+ it('renders contents for slot "actions" within issuable-form component', () => {
+ const buttonEl = wrapper.find(IssuableForm).find('button.js-issuable-save');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('Submit issuable');
+ });
+ });
+});
diff --git a/spec/frontend/issuable_create/components/issuable_form_spec.js b/spec/frontend/issuable_create/components/issuable_form_spec.js
new file mode 100644
index 00000000000..e2c6b4d9521
--- /dev/null
+++ b/spec/frontend/issuable_create/components/issuable_form_spec.js
@@ -0,0 +1,119 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+
+import IssuableForm from '~/issuable_create/components/issuable_form.vue';
+
+const createComponent = ({
+ descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
+ descriptionHelpPath = '/help/user/markdown',
+ labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json',
+ labelsManagePath = '/gitlab-org/gitlab-shell/-/labels',
+} = {}) => {
+ return shallowMount(IssuableForm, {
+ propsData: {
+ descriptionPreviewPath,
+ descriptionHelpPath,
+ labelsFetchPath,
+ labelsManagePath,
+ },
+ slots: {
+ actions: `
+ <button class="js-issuable-save">Submit issuable</button>
+ `,
+ },
+ });
+};
+
+describe('IssuableForm', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('methods', () => {
+ describe('handleUpdateSelectedLabels', () => {
+ it('sets provided `labels` param to prop `selectedLabels`', () => {
+ const labels = [
+ {
+ id: 1,
+ color: '#BADA55',
+ text_color: '#ffffff',
+ title: 'Documentation',
+ },
+ ];
+
+ wrapper.vm.handleUpdateSelectedLabels(labels);
+
+ expect(wrapper.vm.selectedLabels).toBe(labels);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders issuable title input field', () => {
+ const titleFieldEl = wrapper.find('[data-testid="issuable-title"]');
+
+ 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');
+ });
+
+ it('renders issuable description input field', () => {
+ const descriptionFieldEl = wrapper.find('[data-testid="issuable-description"]');
+
+ 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({
+ markdownPreviewPath: wrapper.vm.descriptionPreviewPath,
+ markdownDocsPath: wrapper.vm.descriptionHelpPath,
+ addSpacingClasses: false,
+ showSuggestPopover: true,
+ });
+ expect(descriptionFieldEl.find('textarea').exists()).toBe(true);
+ expect(descriptionFieldEl.find('textarea').attributes('placeholder')).toBe(
+ 'Write a comment or drag your files here…',
+ );
+ });
+
+ it('renders labels select field', () => {
+ const labelsSelectEl = wrapper.find('[data-testid="issuable-labels"]');
+
+ 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({
+ allowLabelEdit: true,
+ allowLabelCreate: true,
+ allowMultiselect: true,
+ allowScopedLabels: true,
+ labelsFetchPath: wrapper.vm.labelsFetchPath,
+ labelsManagePath: wrapper.vm.labelsManagePath,
+ selectedLabels: wrapper.vm.selectedLabels,
+ labelsListTitle: 'Select label',
+ footerCreateLabelTitle: 'Create project label',
+ footerManageLabelTitle: 'Manage project labels',
+ variant: 'embedded',
+ });
+ });
+
+ it('renders contents for slot "actions"', () => {
+ const buttonEl = wrapper
+ .find('[data-testid="issuable-create-actions"]')
+ .find('button.js-issuable-save');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('Submit issuable');
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js
new file mode 100644
index 00000000000..a96a4e15e6c
--- /dev/null
+++ b/spec/frontend/issuable_list/components/issuable_item_spec.js
@@ -0,0 +1,185 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlLabel } from '@gitlab/ui';
+
+import IssuableItem from '~/issuable_list/components/issuable_item.vue';
+
+import { mockIssuable, mockRegularLabel, mockScopedLabel } from '../mock_data';
+
+const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable } = {}) =>
+ shallowMount(IssuableItem, {
+ propsData: {
+ issuableSymbol,
+ issuable,
+ },
+ });
+
+describe('IssuableItem', () => {
+ const mockLabels = mockIssuable.labels.nodes;
+ const mockAuthor = mockIssuable.author;
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('author', () => {
+ it('returns `issuable.author` reference', () => {
+ expect(wrapper.vm.author).toEqual(mockIssuable.author);
+ });
+ });
+
+ describe('authorId', () => {
+ it.each`
+ authorId | returnValue
+ ${1} | ${1}
+ ${'1'} | ${1}
+ ${'gid://gitlab/User/1'} | ${'1'}
+ ${'foo'} | ${''}
+ `(
+ 'returns $returnValue when value of `issuable.author.id` is $authorId',
+ async ({ authorId, returnValue }) => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ author: {
+ ...mockAuthor,
+ id: authorId,
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.authorId).toBe(returnValue);
+ },
+ );
+ });
+
+ describe('labels', () => {
+ it('returns `issuable.labels.nodes` reference when it is available', () => {
+ expect(wrapper.vm.labels).toEqual(mockLabels);
+ });
+
+ it('returns `issuable.labels` reference when it is available', async () => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ labels: mockLabels,
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.labels).toEqual(mockLabels);
+ });
+
+ it('returns empty array when none of `issuable.labels.nodes` or `issuable.labels` are available', async () => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ labels: null,
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.labels).toEqual([]);
+ });
+ });
+
+ describe('createdAt', () => {
+ it('returns string containing timeago string based on `issuable.createdAt`', () => {
+ expect(wrapper.vm.createdAt).toContain('created');
+ expect(wrapper.vm.createdAt).toContain('ago');
+ });
+ });
+
+ describe('updatedAt', () => {
+ it('returns string containing timeago string based on `issuable.updatedAt`', () => {
+ expect(wrapper.vm.updatedAt).toContain('updated');
+ expect(wrapper.vm.updatedAt).toContain('ago');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('scopedLabel', () => {
+ it.each`
+ label | labelType | returnValue
+ ${mockRegularLabel} | ${'regular'} | ${false}
+ ${mockScopedLabel} | ${'scoped'} | ${true}
+ `(
+ 'return $returnValue when provided label param is a $labelType label',
+ ({ label, returnValue }) => {
+ expect(wrapper.vm.scopedLabel(label)).toBe(returnValue);
+ },
+ );
+ });
+ });
+
+ describe('template', () => {
+ it('renders issuable title', () => {
+ const titleEl = wrapper.find('[data-testid="issuable-title"]');
+
+ expect(titleEl.exists()).toBe(true);
+ expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl);
+ expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
+ });
+
+ it('renders issuable reference', () => {
+ const referenceEl = wrapper.find('[data-testid="issuable-reference"]');
+
+ expect(referenceEl.exists()).toBe(true);
+ expect(referenceEl.text()).toBe(`#${mockIssuable.iid}`);
+ });
+
+ it('renders issuable createdAt info', () => {
+ const createdAtEl = wrapper.find('[data-testid="issuable-created-at"]');
+
+ expect(createdAtEl.exists()).toBe(true);
+ expect(createdAtEl.attributes('title')).toBe('Jun 29, 2020 1:52pm GMT+0000');
+ expect(createdAtEl.text()).toBe(wrapper.vm.createdAt);
+ });
+
+ it('renders issuable author info', () => {
+ const authorEl = wrapper.find('[data-testid="issuable-author"]');
+
+ expect(authorEl.exists()).toBe(true);
+ expect(authorEl.attributes()).toMatchObject({
+ 'data-user-id': wrapper.vm.authorId,
+ 'data-username': mockAuthor.username,
+ 'data-name': mockAuthor.name,
+ 'data-avatar-url': mockAuthor.avatarUrl,
+ href: mockAuthor.webUrl,
+ });
+ expect(authorEl.text()).toBe(mockAuthor.name);
+ });
+
+ it('renders gl-label component for each label present within `issuable` prop', () => {
+ const labelsEl = wrapper.findAll(GlLabel);
+
+ expect(labelsEl.exists()).toBe(true);
+ expect(labelsEl).toHaveLength(mockLabels.length);
+ expect(labelsEl.at(0).props()).toMatchObject({
+ backgroundColor: mockLabels[0].color,
+ title: mockLabels[0].title,
+ description: mockLabels[0].description,
+ scoped: false,
+ size: 'sm',
+ });
+ });
+
+ it('renders issuable updatedAt info', () => {
+ const updatedAtEl = wrapper.find('[data-testid="issuable-updated-at"]');
+
+ expect(updatedAtEl.exists()).toBe(true);
+ expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am GMT+0000');
+ expect(updatedAtEl.text()).toBe(wrapper.vm.updatedAt);
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/components/issuable_list_root_spec.js b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
new file mode 100644
index 00000000000..34184522b55
--- /dev/null
+++ b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
@@ -0,0 +1,160 @@
+import { mount } from '@vue/test-utils';
+import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
+
+import IssuableListRoot from '~/issuable_list/components/issuable_list_root.vue';
+import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
+import IssuableItem from '~/issuable_list/components/issuable_item.vue';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+import { mockIssuableListProps } from '../mock_data';
+
+const createComponent = (propsData = mockIssuableListProps) =>
+ mount(IssuableListRoot, {
+ propsData,
+ slots: {
+ 'nav-actions': `
+ <button class="js-new-issuable">New issuable</button>
+ `,
+ 'empty-state': `
+ <p class="js-issuable-empty-state">Issuable empty state</p>
+ `,
+ },
+ });
+
+describe('IssuableListRoot', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('renders component container element with class "issuable-list-container"', () => {
+ expect(wrapper.classes()).toContain('issuable-list-container');
+ });
+
+ it('renders issuable-tabs component', () => {
+ const tabsEl = wrapper.find(IssuableTabs);
+
+ expect(tabsEl.exists()).toBe(true);
+ expect(tabsEl.props()).toMatchObject({
+ tabs: wrapper.vm.tabs,
+ tabCounts: wrapper.vm.tabCounts,
+ currentTab: wrapper.vm.currentTab,
+ });
+ });
+
+ it('renders contents for slot "nav-actions" within issuable-tab component', () => {
+ const buttonEl = wrapper.find(IssuableTabs).find('button.js-new-issuable');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('New issuable');
+ });
+
+ it('renders filtered-search-bar component', () => {
+ const searchEl = wrapper.find(FilteredSearchBar);
+ const {
+ namespace,
+ recentSearchesStorageKey,
+ searchInputPlaceholder,
+ searchTokens,
+ sortOptions,
+ initialFilterValue,
+ initialSortBy,
+ } = wrapper.vm;
+
+ expect(searchEl.exists()).toBe(true);
+ expect(searchEl.props()).toMatchObject({
+ namespace,
+ recentSearchesStorageKey,
+ searchInputPlaceholder,
+ tokens: searchTokens,
+ sortOptions,
+ initialFilterValue,
+ initialSortBy,
+ });
+ });
+
+ it('renders gl-loading-icon when `issuablesLoading` prop is true', async () => {
+ wrapper.setProps({
+ issuablesLoading: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders issuable-item component for each item within `issuables` array', () => {
+ const itemsEl = wrapper.findAll(IssuableItem);
+ const mockIssuable = mockIssuableListProps.issuables[0];
+
+ expect(itemsEl).toHaveLength(mockIssuableListProps.issuables.length);
+ expect(itemsEl.at(0).props()).toMatchObject({
+ issuableSymbol: wrapper.vm.issuableSymbol,
+ issuable: mockIssuable,
+ });
+ });
+
+ it('renders contents for slot "empty-state" when `issuablesLoading` is false and `issuables` is empty', async () => {
+ wrapper.setProps({
+ issuables: [],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find('p.js-issuable-empty-state').exists()).toBe(true);
+ expect(wrapper.find('p.js-issuable-empty-state').text()).toBe('Issuable empty state');
+ });
+
+ it('renders gl-pagination when `showPaginationControls` prop is true', async () => {
+ wrapper.setProps({
+ showPaginationControls: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ const paginationEl = wrapper.find(GlPagination);
+ expect(paginationEl.exists()).toBe(true);
+ expect(paginationEl.props()).toMatchObject({
+ perPage: 20,
+ value: 1,
+ prevPage: 0,
+ nextPage: 2,
+ align: 'center',
+ });
+ });
+ });
+
+ describe('events', () => {
+ it('issuable-tabs component emits `click-tab` event on `click-tab` event', () => {
+ wrapper.find(IssuableTabs).vm.$emit('click');
+
+ expect(wrapper.emitted('click-tab')).toBeTruthy();
+ });
+
+ it('filtered-search-bar component emits `filter` event on `onFilter` & `sort` event on `onSort` events', () => {
+ const searchEl = wrapper.find(FilteredSearchBar);
+
+ searchEl.vm.$emit('onFilter');
+ expect(wrapper.emitted('filter')).toBeTruthy();
+ searchEl.vm.$emit('onSort');
+ expect(wrapper.emitted('sort')).toBeTruthy();
+ });
+
+ it('gl-pagination component emits `page-change` event on `input` event', async () => {
+ wrapper.setProps({
+ showPaginationControls: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.find(GlPagination).vm.$emit('input');
+ expect(wrapper.emitted('page-change')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/components/issuable_tabs_spec.js b/spec/frontend/issuable_list/components/issuable_tabs_spec.js
new file mode 100644
index 00000000000..12611400084
--- /dev/null
+++ b/spec/frontend/issuable_list/components/issuable_tabs_spec.js
@@ -0,0 +1,91 @@
+import { mount } from '@vue/test-utils';
+import { GlTab, GlBadge } from '@gitlab/ui';
+
+import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
+
+import { mockIssuableListProps } from '../mock_data';
+
+const createComponent = ({
+ tabs = mockIssuableListProps.tabs,
+ tabCounts = mockIssuableListProps.tabCounts,
+ currentTab = mockIssuableListProps.currentTab,
+} = {}) =>
+ mount(IssuableTabs, {
+ propsData: {
+ tabs,
+ tabCounts,
+ currentTab,
+ },
+ slots: {
+ 'nav-actions': `
+ <button class="js-new-issuable">New issuable</button>
+ `,
+ },
+ });
+
+describe('IssuableTabs', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('methods', () => {
+ describe('isTabActive', () => {
+ it.each`
+ tabName | currentTab | returnValue
+ ${'opened'} | ${'opened'} | ${true}
+ ${'opened'} | ${'closed'} | ${false}
+ `(
+ 'returns $returnValue when tab name is "$tabName" is current tab is "$currentTab"',
+ async ({ tabName, currentTab, returnValue }) => {
+ wrapper.setProps({
+ currentTab,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.isTabActive(tabName)).toBe(returnValue);
+ },
+ );
+ });
+ });
+
+ describe('template', () => {
+ it('renders gl-tab for each tab within `tabs` array', () => {
+ const tabsEl = wrapper.findAll(GlTab);
+
+ expect(tabsEl.exists()).toBe(true);
+ expect(tabsEl).toHaveLength(mockIssuableListProps.tabs.length);
+ });
+
+ it('renders gl-badge component within a tab', () => {
+ const badgeEl = wrapper.findAll(GlBadge).at(0);
+
+ expect(badgeEl.exists()).toBe(true);
+ expect(badgeEl.text()).toBe(`${mockIssuableListProps.tabCounts.opened}`);
+ });
+
+ it('renders contents for slot "nav-actions"', () => {
+ const buttonEl = wrapper.find('button.js-new-issuable');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('New issuable');
+ });
+ });
+
+ describe('events', () => {
+ it('gl-tab component emits `click` event on `click` event', () => {
+ const tabEl = wrapper.findAll(GlTab).at(0);
+
+ tabEl.vm.$emit('click', 'opened');
+
+ expect(wrapper.emitted('click')).toBeTruthy();
+ expect(wrapper.emitted('click')[0]).toEqual(['opened']);
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/mock_data.js b/spec/frontend/issuable_list/mock_data.js
new file mode 100644
index 00000000000..f6f914a595d
--- /dev/null
+++ b/spec/frontend/issuable_list/mock_data.js
@@ -0,0 +1,135 @@
+import {
+ mockAuthorToken,
+ mockLabelToken,
+ mockSortOptions,
+} from 'jest/vue_shared/components/filtered_search_bar/mock_data';
+
+export const mockAuthor = {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://0.0.0.0:3000/root',
+};
+
+export const mockRegularLabel = {
+ id: 'gid://gitlab/GroupLabel/2048',
+ title: 'Documentation Update',
+ description: null,
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+};
+
+export const mockScopedLabel = {
+ id: 'gid://gitlab/ProjectLabel/2049',
+ title: 'status::confirmed',
+ description: null,
+ color: '#D9534F',
+ textColor: '#FFFFFF',
+};
+
+export const mockLabels = [mockRegularLabel, mockScopedLabel];
+
+export const mockIssuable = {
+ iid: '30',
+ title: 'Dismiss Cipher with no integrity',
+ description: null,
+ createdAt: '2020-06-29T13:52:56Z',
+ updatedAt: '2020-09-10T11:41:13Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/30',
+ author: mockAuthor,
+ labels: {
+ nodes: mockLabels,
+ },
+};
+
+export const mockIssuables = [
+ mockIssuable,
+ {
+ iid: '28',
+ title: 'Dismiss Cipher with no integrity',
+ description: null,
+ createdAt: '2020-06-29T13:52:56Z',
+ updatedAt: '2020-06-29T13:52:56Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/28',
+ author: mockAuthor,
+ labels: {
+ nodes: [],
+ },
+ },
+ {
+ iid: '7',
+ title: 'Temporibus in veritatis labore explicabo velit molestiae sed.',
+ description: 'Quo consequatur rem aliquid laborum quibusdam molestiae saepe.',
+ createdAt: '2020-06-25T13:50:14Z',
+ updatedAt: '2020-08-25T06:09:27Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/7',
+ author: mockAuthor,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ {
+ iid: '17',
+ title: 'Vel voluptatem quaerat est hic incidunt qui ut aliquid sit exercitationem.',
+ description: 'Incidunt accusamus perspiciatis aut excepturi.',
+ createdAt: '2020-06-19T13:51:36Z',
+ updatedAt: '2020-08-11T13:36:35Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/17',
+ author: mockAuthor,
+ labels: {
+ nodes: [],
+ },
+ },
+ {
+ iid: '16',
+ title: 'Vero qui quo labore libero omnis quisquam et cumque.',
+ description: 'Ipsa ipsum magni nostrum alias aut exercitationem.',
+ createdAt: '2020-06-19T13:51:36Z',
+ updatedAt: '2020-06-19T13:51:36Z',
+ webUrl: 'http://0.0.0.0:3000/gitlab-org/gitlab-shell/-/issues/16',
+ author: mockAuthor,
+ labels: {
+ nodes: [],
+ },
+ },
+];
+
+export const mockTabs = [
+ {
+ id: 'state-opened',
+ name: 'opened',
+ title: 'Open',
+ titleTooltip: 'Filter by issuables that are currently opened.',
+ },
+ {
+ id: 'state-archived',
+ name: 'closed',
+ title: 'Closed',
+ titleTooltip: 'Filter by issuables that are currently archived.',
+ },
+ {
+ id: 'state-all',
+ name: 'all',
+ title: 'All',
+ titleTooltip: 'Show all issuables.',
+ },
+];
+
+export const mockTabCounts = {
+ opened: 5,
+ closed: 0,
+ all: 5,
+};
+
+export const mockIssuableListProps = {
+ namespace: 'gitlab-org/gitlab-test',
+ recentSearchesStorageKey: 'issues',
+ searchInputPlaceholder: 'Search issues',
+ searchTokens: [mockAuthorToken, mockLabelToken],
+ sortOptions: mockSortOptions,
+ issuables: mockIssuables,
+ tabs: mockTabs,
+ tabCounts: mockTabCounts,
+ currentTab: 'opened',
+};
diff --git a/spec/frontend/issuable_suggestions/components/app_spec.js b/spec/frontend/issuable_suggestions/components/app_spec.js
index d51c89807be..0cb5b9c90ba 100644
--- a/spec/frontend/issuable_suggestions/components/app_spec.js
+++ b/spec/frontend/issuable_suggestions/components/app_spec.js
@@ -41,7 +41,7 @@ describe('Issuable suggestions app component', () => {
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
- expect(wrapper.isEmpty()).toBe(false);
+ expect(wrapper.findAll('li').length).toBe(data.issues.length);
});
});
@@ -89,8 +89,8 @@ describe('Issuable suggestions app component', () => {
wrapper
.findAll('li')
.at(0)
- .is('.gl-mb-3'),
- ).toBe(true);
+ .classes(),
+ ).toContain('gl-mb-3');
});
});
@@ -102,8 +102,8 @@ describe('Issuable suggestions app component', () => {
wrapper
.findAll('li')
.at(1)
- .is('.gl-mb-3'),
- ).toBe(false);
+ .classes(),
+ ).not.toContain('gl-mb-3');
});
});
});
diff --git a/spec/frontend/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index ad37ccd2ca5..9912e77d5fe 100644
--- a/spec/frontend/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { GlTooltip, GlLink } from '@gitlab/ui';
+import { GlTooltip, GlLink, GlIcon } from '@gitlab/ui';
import { TEST_HOST } from 'jest/helpers/test_constants';
-import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import Suggestion from '~/issuable_suggestions/components/item.vue';
import mockData from '../mock_data';
@@ -48,7 +47,7 @@ describe('Issuable suggestions suggestion component', () => {
it('renders icon', () => {
createComponent();
- const icon = vm.find(Icon);
+ const icon = vm.find(GlIcon);
expect(icon.props('name')).toBe('issue-open-m');
});
@@ -71,7 +70,7 @@ describe('Issuable suggestions suggestion component', () => {
state: 'closed',
});
- const icon = vm.find(Icon);
+ const icon = vm.find(GlIcon);
expect(icon.props('name')).toBe('issue-close');
});
@@ -112,7 +111,7 @@ describe('Issuable suggestions suggestion component', () => {
const count = vm.findAll('.suggestion-counts span').at(0);
expect(count.text()).toContain('1');
- expect(count.find(Icon).props('name')).toBe('thumb-up');
+ expect(count.find(GlIcon).props('name')).toBe('thumb-up');
});
it('renders notes count', () => {
@@ -121,7 +120,7 @@ describe('Issuable suggestions suggestion component', () => {
const count = vm.findAll('.suggestion-counts span').at(1);
expect(count.text()).toContain('2');
- expect(count.find(Icon).props('name')).toBe('comment');
+ expect(count.find(GlIcon).props('name')).toBe('comment');
});
});
@@ -131,7 +130,7 @@ describe('Issuable suggestions suggestion component', () => {
confidential: true,
});
- const icon = vm.find(Icon);
+ const icon = vm.find(GlIcon);
expect(icon.props('name')).toBe('eye-slash');
expect(icon.attributes('title')).toBe('Confidential');
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index f76f42cb9ae..f4095d4de96 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -1,14 +1,22 @@
import { GlIntersectionObserver } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import '~/behaviors/markdown/render_gfm';
import IssuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
-import { initialRequest, secondRequest } from '../mock_data';
+import {
+ appProps,
+ initialRequest,
+ publishedIncidentUrl,
+ secondRequest,
+ zoomMeetingUrl,
+} from '../mock_data';
+import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
+import DescriptionComponent from '~/issue_show/components/description.vue';
+import PinnedLinks from '~/issue_show/components/pinned_links.vue';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
@@ -19,9 +27,6 @@ jest.mock('~/issue_show/event_hub');
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
-const zoomMeetingUrl = 'https://gitlab.zoom.us/j/95919234811';
-const publishedIncidentUrl = 'https://status.com/';
-
describe('Issuable output', () => {
useMockIntersectionObserver();
@@ -31,6 +36,21 @@ describe('Issuable output', () => {
const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]');
+ const mountComponent = (props = {}, options = {}) => {
+ wrapper = mount(IssuableApp, {
+ propsData: { ...appProps, ...props },
+ provide: {
+ fullPath: 'gitlab-org/incidents',
+ iid: '19',
+ },
+ stubs: {
+ HighlightBar: true,
+ IncidentTabs: true,
+ },
+ ...options,
+ });
+ };
+
beforeEach(() => {
setFixtures(`
<div>
@@ -57,28 +77,9 @@ describe('Issuable output', () => {
return res;
});
- wrapper = mount(IssuableApp, {
- propsData: {
- canUpdate: true,
- canDestroy: true,
- endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
- updateEndpoint: TEST_HOST,
- issuableRef: '#1',
- issuableStatus: 'opened',
- initialTitleHtml: '',
- initialTitleText: '',
- initialDescriptionHtml: 'test',
- initialDescriptionText: 'test',
- lockVersion: 1,
- markdownPreviewPath: '/',
- markdownDocsPath: '/',
- projectNamespace: '/',
- projectPath: '/',
- issuableTemplateNamesPath: '/issuable-templates-path',
- zoomMeetingUrl,
- publishedIncidentUrl,
- },
- });
+ mountComponent();
+
+ jest.advanceTimersByTime(2);
});
afterEach(() => {
@@ -134,7 +135,7 @@ describe('Issuable output', () => {
wrapper.vm.showForm = true;
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.contains('.markdown-selector')).toBe(true);
+ expect(wrapper.find('.markdown-selector').exists()).toBe(true);
});
});
@@ -143,7 +144,7 @@ describe('Issuable output', () => {
wrapper.setProps({ canUpdate: false });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.contains('.markdown-selector')).toBe(false);
+ expect(wrapper.find('.markdown-selector').exists()).toBe(false);
});
});
@@ -403,7 +404,7 @@ describe('Issuable output', () => {
.then(() => {
expect(wrapper.vm.formState.lockedWarningVisible).toEqual(true);
expect(wrapper.vm.formState.lock_version).toEqual(1);
- expect(wrapper.contains('.alert')).toBe(true);
+ expect(wrapper.find('.alert').exists()).toBe(true);
});
});
});
@@ -441,14 +442,14 @@ describe('Issuable output', () => {
describe('show inline edit button', () => {
it('should not render by default', () => {
- expect(wrapper.contains('.btn-edit')).toBe(true);
+ expect(wrapper.find('.btn-edit').exists()).toBe(true);
});
it('should render if showInlineEditButton', () => {
wrapper.setProps({ showInlineEditButton: true });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.contains('.btn-edit')).toBe(true);
+ expect(wrapper.find('.btn-edit').exists()).toBe(true);
});
});
});
@@ -531,7 +532,7 @@ describe('Issuable output', () => {
describe('sticky header', () => {
describe('when title is in view', () => {
it('is not shown', () => {
- expect(wrapper.contains('.issue-sticky-header')).toBe(false);
+ expect(wrapper.find('.issue-sticky-header').exists()).toBe(false);
});
});
@@ -562,4 +563,59 @@ describe('Issuable output', () => {
});
});
});
+
+ describe('Composable description component', () => {
+ const findIncidentTabs = () => wrapper.find(IncidentTabs);
+ const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
+ const findPinnedLinks = () => wrapper.find(PinnedLinks);
+ const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6';
+
+ describe('when using description component', () => {
+ it('renders the description component', () => {
+ expect(findDescriptionComponent().exists()).toBe(true);
+ });
+
+ it('does not render incident tabs', () => {
+ expect(findIncidentTabs().exists()).toBe(false);
+ });
+
+ it('adds a border below the header', () => {
+ expect(findPinnedLinks().attributes('class')).toContain(borderClass);
+ });
+ });
+
+ describe('when using incident tabs description wrapper', () => {
+ beforeEach(() => {
+ mountComponent(
+ {
+ descriptionComponent: IncidentTabs,
+ showTitleBorder: false,
+ },
+ {
+ mocks: {
+ $apollo: {
+ queries: {
+ alert: {
+ loading: false,
+ },
+ },
+ },
+ },
+ },
+ );
+ });
+
+ it('renders the description component', () => {
+ expect(findDescriptionComponent().exists()).toBe(true);
+ });
+
+ it('renders incident tabs', () => {
+ expect(findIncidentTabs().exists()).toBe(true);
+ });
+
+ it('does not add a border below the header', () => {
+ expect(findPinnedLinks().attributes('class')).not.toContain(borderClass);
+ });
+ });
+ });
});
diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js
index 0053475dd13..bc7511225a0 100644
--- a/spec/frontend/issue_show/components/description_spec.js
+++ b/spec/frontend/issue_show/components/description_spec.js
@@ -5,20 +5,13 @@ import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants';
import Description from '~/issue_show/components/description.vue';
import TaskList from '~/task_list';
+import { descriptionProps as props } from '../mock_data';
jest.mock('~/task_list');
describe('Description component', () => {
let vm;
let DescriptionComponent;
- const props = {
- canUpdate: true,
- descriptionHtml: 'test',
- descriptionText: 'test',
- updatedAt: new Date().toString(),
- taskStatus: '',
- updateUrl: TEST_HOST,
- };
beforeEach(() => {
DescriptionComponent = Vue.extend(Description);
@@ -43,12 +36,27 @@ describe('Description component', () => {
$('.issuable-meta .flash-container').remove();
});
- it('animates description changes', () => {
+ it('doesnt animate first description changes', () => {
vm.descriptionHtml = 'changed';
+ return vm.$nextTick().then(() => {
+ expect(
+ vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
+ ).toBeFalsy();
+ jest.runAllTimers();
+ return vm.$nextTick();
+ });
+ });
+
+ it('animates description changes on live update', () => {
+ vm.descriptionHtml = 'changed';
return vm
.$nextTick()
.then(() => {
+ vm.descriptionHtml = 'changed second time';
+ return vm.$nextTick();
+ })
+ .then(() => {
expect(
vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
).toBeTruthy();
diff --git a/spec/frontend/issue_show/components/edit_actions_spec.js b/spec/frontend/issue_show/components/edit_actions_spec.js
index b0c1894058e..79a2bcd5eab 100644
--- a/spec/frontend/issue_show/components/edit_actions_spec.js
+++ b/spec/frontend/issue_show/components/edit_actions_spec.js
@@ -70,16 +70,6 @@ describe('Edit Actions components', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
- it('shows loading icon after clicking save button', done => {
- vm.$el.querySelector('.btn-success').click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-success .fa')).not.toBeNull();
-
- done();
- });
- });
-
it('disabled button after clicking save button', done => {
vm.$el.querySelector('.btn-success').click();
@@ -107,17 +97,6 @@ describe('Edit Actions components', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true });
});
- it('shows loading icon after clicking delete button', done => {
- jest.spyOn(window, 'confirm').mockReturnValue(true);
- vm.$el.querySelector('.btn-danger').click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-danger .fa')).not.toBeNull();
-
- done();
- });
- });
-
it('does no actions when confirm is false', done => {
jest.spyOn(window, 'confirm').mockReturnValue(false);
vm.$el.querySelector('.btn-danger').click();
diff --git a/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js b/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js
new file mode 100644
index 00000000000..8d50df5e406
--- /dev/null
+++ b/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js
@@ -0,0 +1,56 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+jest.mock('~/lib/utils/datetime_utility');
+
+describe('Highlight Bar', () => {
+ let wrapper;
+
+ const alert = {
+ startedAt: '2020-05-29T10:39:22Z',
+ detailsUrl: 'http://127.0.0.1:3000/root/unique-alerts/-/alert_management/1/details',
+ eventCount: 1,
+ title: 'Alert 1',
+ };
+
+ const mountComponent = () => {
+ wrapper = shallowMount(HighlightBar, {
+ propsData: {
+ alert,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findLink = () => wrapper.find(GlLink);
+
+ it('renders a link to the alert page', () => {
+ expect(findLink().exists()).toBe(true);
+ expect(findLink().attributes('href')).toBe(alert.detailsUrl);
+ expect(findLink().text()).toContain(alert.title);
+ });
+
+ it('renders formatted start time of the alert', () => {
+ const formattedDate = '2020-05-29 UTC';
+ formatDate.mockReturnValueOnce(formattedDate);
+ mountComponent();
+ expect(formatDate).toHaveBeenCalledWith(alert.startedAt, 'yyyy-mm-dd Z');
+ expect(wrapper.text()).toContain(formattedDate);
+ });
+
+ it('renders a number of alert events', () => {
+ expect(wrapper.text()).toContain(alert.eventCount);
+ });
+});
diff --git a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
new file mode 100644
index 00000000000..a51b497cd79
--- /dev/null
+++ b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
@@ -0,0 +1,101 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlTab } from '@gitlab/ui';
+import INVALID_URL from '~/lib/utils/invalid_url';
+import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
+import { descriptionProps } from '../../mock_data';
+import DescriptionComponent from '~/issue_show/components/description.vue';
+import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+
+const mockAlert = {
+ __typename: 'AlertManagementAlert',
+ detailsUrl: INVALID_URL,
+ iid: '1',
+};
+
+describe('Incident Tabs component', () => {
+ let wrapper;
+
+ const mountComponent = (data = {}) => {
+ wrapper = shallowMount(IncidentTabs, {
+ propsData: {
+ ...descriptionProps,
+ },
+ stubs: {
+ DescriptionComponent: true,
+ },
+ provide: {
+ fullPath: '',
+ iid: '',
+ },
+ data() {
+ return { alert: mockAlert, ...data };
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ alert: {
+ loading: true,
+ },
+ },
+ },
+ },
+ });
+ };
+
+ const findTabs = () => wrapper.findAll(GlTab);
+ const findSummaryTab = () => findTabs().at(0);
+ const findAlertDetailsTab = () => findTabs().at(1);
+ const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
+ const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
+ const findHighlightBarComponent = () => wrapper.find(HighlightBar);
+
+ describe('empty state', () => {
+ beforeEach(() => {
+ mountComponent({ alert: null });
+ });
+
+ it('does not show the alert details tab', () => {
+ expect(findAlertDetailsComponent().exists()).toBe(false);
+ expect(findHighlightBarComponent().exists()).toBe(false);
+ });
+ });
+
+ describe('with an alert present', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders the summary tab', () => {
+ expect(findSummaryTab().exists()).toBe(true);
+ expect(findSummaryTab().attributes('title')).toBe('Summary');
+ });
+
+ it('renders the alert details tab', () => {
+ expect(findAlertDetailsTab().exists()).toBe(true);
+ expect(findAlertDetailsTab().attributes('title')).toBe('Alert details');
+ });
+
+ it('renders the alert details table with the correct props', () => {
+ const alert = { iid: mockAlert.iid };
+
+ expect(findAlertDetailsComponent().props('alert')).toEqual(alert);
+ expect(findAlertDetailsComponent().props('loading')).toBe(true);
+ });
+
+ it('renders the description component with highlight bar', () => {
+ expect(findDescriptionComponent().exists()).toBe(true);
+ expect(findHighlightBarComponent().exists()).toBe(true);
+ });
+
+ it('renders the highlight bar component with the correct props', () => {
+ const alert = { detailsUrl: mockAlert.detailsUrl };
+
+ expect(findHighlightBarComponent().props('alert')).toMatchObject(alert);
+ });
+
+ it('passes all props to the description component', () => {
+ expect(findDescriptionComponent().props()).toMatchObject(descriptionProps);
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/helpers.js b/spec/frontend/issue_show/helpers.js
index 5d2ced98ae4..7ca6a22929d 100644
--- a/spec/frontend/issue_show/helpers.js
+++ b/spec/frontend/issue_show/helpers.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const keyboardDownEvent = (code, metaKey = false, ctrlKey = false) => {
const e = new CustomEvent('keydown');
diff --git a/spec/frontend/issue_show/index_spec.js b/spec/frontend/issue_show/index_spec.js
deleted file mode 100644
index e80d1b83c11..00000000000
--- a/spec/frontend/issue_show/index_spec.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import initIssueableApp from '~/issue_show';
-
-describe('Issue show index', () => {
- describe('initIssueableApp', () => {
- it('should initialize app with no potential XSS attack', () => {
- const d = document.createElement('div');
- d.id = 'js-issuable-app-initial-data';
- d.innerHTML = JSON.stringify({
- initialDescriptionHtml: '&lt;img src=x onerror=alert(1)&gt;',
- });
- document.body.appendChild(d);
-
- const alertSpy = jest.spyOn(window, 'alert');
- initIssueableApp();
-
- expect(alertSpy).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/issue_show/issue_spec.js b/spec/frontend/issue_show/issue_spec.js
new file mode 100644
index 00000000000..befb670c6cd
--- /dev/null
+++ b/spec/frontend/issue_show/issue_spec.js
@@ -0,0 +1,45 @@
+import MockAdapter from 'axios-mock-adapter';
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import initIssuableApp from '~/issue_show/issue';
+import * as parseData from '~/issue_show/utils/parse_data';
+import { appProps } from './mock_data';
+
+const mock = new MockAdapter(axios);
+mock.onGet().reply(200);
+
+useMockIntersectionObserver();
+
+jest.mock('~/lib/utils/poll');
+
+const setupHTML = initialData => {
+ document.body.innerHTML = `
+ <div id="js-issuable-app"></div>
+ <script id="js-issuable-app-initial-data" type="application/json">
+ ${JSON.stringify(initialData)}
+ </script>
+ `;
+};
+
+describe('Issue show index', () => {
+ describe('initIssueableApp', () => {
+ it('should initialize app with no potential XSS attack', async () => {
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+ const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData');
+
+ setupHTML({
+ ...appProps,
+ initialDescriptionHtml: '<svg onload=window.alert(1)>',
+ });
+
+ const issuableData = parseData.parseIssuableData();
+ initIssuableApp(issuableData);
+
+ await waitForPromises();
+
+ expect(parseDataSpy).toHaveBeenCalled();
+ expect(alertSpy).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/mock_data.js b/spec/frontend/issue_show/mock_data.js
index ff01a004186..5a31a550088 100644
--- a/spec/frontend/issue_show/mock_data.js
+++ b/spec/frontend/issue_show/mock_data.js
@@ -1,3 +1,5 @@
+import { TEST_HOST } from 'helpers/test_constants';
+
export const initialRequest = {
title: '<p>this is a title</p>',
title_text: 'this is a title',
@@ -21,3 +23,36 @@ export const secondRequest = {
updated_by_path: '/other_user',
lock_version: 2,
};
+
+export const descriptionProps = {
+ canUpdate: true,
+ descriptionHtml: 'test',
+ descriptionText: 'test',
+ taskStatus: '',
+ updateUrl: TEST_HOST,
+};
+
+export const publishedIncidentUrl = 'https://status.com/';
+
+export const zoomMeetingUrl = 'https://gitlab.zoom.us/j/95919234811';
+
+export const appProps = {
+ canUpdate: true,
+ canDestroy: true,
+ endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
+ updateEndpoint: TEST_HOST,
+ issuableRef: '#1',
+ issuableStatus: 'opened',
+ initialTitleHtml: '',
+ initialTitleText: '',
+ initialDescriptionHtml: 'test',
+ initialDescriptionText: 'test',
+ lockVersion: 1,
+ markdownPreviewPath: '/',
+ markdownDocsPath: '/',
+ projectNamespace: '/',
+ projectPath: '/',
+ issuableTemplateNamesPath: '/issuable-templates-path',
+ zoomMeetingUrl,
+ publishedIncidentUrl,
+};
diff --git a/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap b/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap
index c327b7de827..c327b7de827 100644
--- a/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap
+++ b/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap
diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issues_list/components/issuable_spec.js
index 6ede46a602a..c20684cc385 100644
--- a/spec/frontend/issuables_list/components/issuable_spec.js
+++ b/spec/frontend/issues_list/components/issuable_spec.js
@@ -5,7 +5,7 @@ import { trimText } from 'helpers/text_helper';
import initUserPopovers from '~/user_popovers';
import { formatDate } from '~/lib/utils/datetime_utility';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import Issuable from '~/issuables_list/components/issuable.vue';
+import Issuable from '~/issues_list/components/issuable.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -52,7 +52,6 @@ describe('Issuable component', () => {
},
stubs: {
'gl-sprintf': GlSprintf,
- 'gl-link': '<a><slot></slot></a>',
},
});
};
@@ -98,7 +97,7 @@ describe('Issuable component', () => {
const findUnscopedLabels = () => findLabels().filter(w => !isScopedLabel({ title: w.text() }));
const findIssuableTitle = () => wrapper.find('[data-testid="issuable-title"]');
const findIssuableStatus = () => wrapper.find('[data-testid="issuable-status"]');
- const containsJiraLogo = () => wrapper.contains('[data-testid="jira-logo"]');
+ const containsJiraLogo = () => wrapper.find('[data-testid="jira-logo"]').exists();
const findHealthStatus = () => wrapper.find('.health-status');
describe('when mounted', () => {
diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js
index 65b87ddf6a6..1f80b4fc54a 100644
--- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issuables_list_app_spec.js
@@ -1,18 +1,22 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
-import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
+import {
+ GlEmptyState,
+ GlPagination,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+} from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import { deprecatedCreateFlash as flash } from '~/flash';
-import IssuablesListApp from '~/issuables_list/components/issuables_list_app.vue';
-import Issuable from '~/issuables_list/components/issuable.vue';
+import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
+import Issuable from '~/issues_list/components/issuable.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import issueablesEventBus from '~/issuables_list/eventhub';
-import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issuables_list/constants';
+import issueablesEventBus from '~/issues_list/eventhub';
+import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants';
jest.mock('~/flash');
-jest.mock('~/issuables_list/eventhub');
+jest.mock('~/issues_list/eventhub');
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
scrollToElement: () => {},
@@ -169,7 +173,7 @@ describe('Issuables list component', () => {
it('does not display empty state', () => {
expect(wrapper.vm.issuables.length).toBeGreaterThan(0);
expect(wrapper.vm.emptyState).toEqual({});
- expect(wrapper.contains(GlEmptyState)).toBe(false);
+ expect(wrapper.find(GlEmptyState).exists()).toBe(false);
});
it('sets the proper page and total items', () => {
diff --git a/spec/frontend/issuables_list/components/issuable_list_root_app_spec.js b/spec/frontend/issues_list/components/jira_issues_list_root_spec.js
index aee49076b5d..eecb092a330 100644
--- a/spec/frontend/issuables_list/components/issuable_list_root_app_spec.js
+++ b/spec/frontend/issues_list/components/jira_issues_list_root_spec.js
@@ -1,9 +1,9 @@
import { GlAlert, GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import IssuableListRootApp from '~/issuables_list/components/issuable_list_root_app.vue';
+import JiraIssuesListRoot from '~/issues_list/components/jira_issues_list_root.vue';
-describe('IssuableListRootApp', () => {
+describe('JiraIssuesListRoot', () => {
const issuesPath = 'gitlab-org/gitlab-test/-/issues';
const label = {
color: '#333',
@@ -19,7 +19,7 @@ describe('IssuableListRootApp', () => {
shouldShowFinishedAlert = false,
shouldShowInProgressAlert = false,
} = {}) =>
- shallowMount(IssuableListRootApp, {
+ shallowMount(JiraIssuesListRoot, {
propsData: {
canEdit: true,
isJiraConfigured: true,
@@ -47,7 +47,7 @@ describe('IssuableListRootApp', () => {
it('does not show an alert', () => {
wrapper = mountComponent();
- expect(wrapper.contains(GlAlert)).toBe(false);
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
});
});
@@ -103,12 +103,12 @@ describe('IssuableListRootApp', () => {
shouldShowInProgressAlert: true,
});
- expect(wrapper.contains(GlAlert)).toBe(true);
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
findAlert().vm.$emit('dismiss');
return Vue.nextTick(() => {
- expect(wrapper.contains(GlAlert)).toBe(false);
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/issuables_list/issuable_list_test_data.js b/spec/frontend/issues_list/issuable_list_test_data.js
index 313aa15bd31..313aa15bd31 100644
--- a/spec/frontend/issuables_list/issuable_list_test_data.js
+++ b/spec/frontend/issues_list/issuable_list_test_data.js
diff --git a/spec/frontend/issues_list/service_desk_helper_spec.js b/spec/frontend/issues_list/service_desk_helper_spec.js
new file mode 100644
index 00000000000..16aee853341
--- /dev/null
+++ b/spec/frontend/issues_list/service_desk_helper_spec.js
@@ -0,0 +1,28 @@
+import { emptyStateHelper, generateMessages } from '~/issues_list/service_desk_helper';
+
+describe('service desk helper', () => {
+ const emptyStateMessages = generateMessages({});
+
+ // Note: isServiceDeskEnabled must not be true when isServiceDeskSupported is false (it's an invalid case).
+ describe.each`
+ isServiceDeskSupported | isServiceDeskEnabled | canEditProjectSettings | expectedMessage
+ ${true} | ${true} | ${true} | ${'serviceDeskEnabledAndCanEditProjectSettings'}
+ ${true} | ${true} | ${false} | ${'serviceDeskEnabledAndCannotEditProjectSettings'}
+ ${true} | ${false} | ${true} | ${'serviceDeskDisabledAndCanEditProjectSettings'}
+ ${true} | ${false} | ${false} | ${'serviceDeskDisabledAndCannotEditProjectSettings'}
+ ${false} | ${false} | ${true} | ${'serviceDeskIsNotSupported'}
+ ${false} | ${false} | ${false} | ${'serviceDeskIsNotEnabled'}
+ `(
+ 'isServiceDeskSupported = $isServiceDeskSupported, isServiceDeskEnabled = $isServiceDeskEnabled, canEditProjectSettings = $canEditProjectSettings',
+ ({ isServiceDeskSupported, isServiceDeskEnabled, canEditProjectSettings, expectedMessage }) => {
+ it(`displays ${expectedMessage} message`, () => {
+ const emptyStateMeta = {
+ isServiceDeskEnabled,
+ isServiceDeskSupported,
+ canEditProjectSettings,
+ };
+ expect(emptyStateHelper(emptyStateMeta)).toEqual(emptyStateMessages[expectedMessage]);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index 975c31bb59c..eede5426f42 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -114,7 +114,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<div
- class="gl-search-box-by-type m-2"
+ class="gl-search-box-by-type gl-m-3"
>
<svg
class="gl-search-box-by-type-search-icon gl-icon s16"
@@ -225,7 +225,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<div
- class="gl-search-box-by-type m-2"
+ class="gl-search-box-by-type gl-m-3"
>
<svg
class="gl-search-box-by-type-search-icon gl-icon s16"
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 6ef28a71f48..d184c054b8a 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlButton, GlNewDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
+import { GlAlert, GlButton, GlDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -35,7 +35,7 @@ describe('JiraImportForm', () => {
const getTable = () => wrapper.find(GlTable);
- const getUserDropdown = () => getTable().find(GlNewDropdown);
+ const getUserDropdown = () => getTable().find(GlDropdown);
const getHeader = name => getByRole(wrapper.element, 'columnheader', { name });
@@ -100,7 +100,7 @@ describe('JiraImportForm', () => {
it('is shown', () => {
wrapper = mountComponent();
- expect(wrapper.contains(GlFormSelect)).toBe(true);
+ expect(wrapper.find(GlFormSelect).exists()).toBe(true);
});
it('contains a list of Jira projects to select from', () => {
diff --git a/spec/frontend/jira_import/utils/jira_import_utils_spec.js b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
index 8ae1fc3535a..0992c9e8d16 100644
--- a/spec/frontend/jira_import/utils/jira_import_utils_spec.js
+++ b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
@@ -8,7 +8,7 @@ import {
setFinishedAlertHideMap,
shouldShowFinishedAlert,
} from '~/jira_import/utils/jira_import_utils';
-import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issuables_list/constants';
+import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
useLocalStorageSpy();
diff --git a/spec/frontend/jobs/components/artifacts_block_spec.js b/spec/frontend/jobs/components/artifacts_block_spec.js
index 11bd645916e..a709a59cadd 100644
--- a/spec/frontend/jobs/components/artifacts_block_spec.js
+++ b/spec/frontend/jobs/components/artifacts_block_spec.js
@@ -8,7 +8,10 @@ describe('Artifacts block', () => {
const createWrapper = propsData =>
mount(ArtifactsBlock, {
- propsData,
+ propsData: {
+ helpUrl: 'help-url',
+ ...propsData,
+ },
});
const findArtifactRemoveElt = () => wrapper.find('[data-testid="artifacts-remove-timeline"]');
@@ -68,6 +71,12 @@ describe('Artifacts block', () => {
expect(trimText(findArtifactRemoveElt().text())).toBe(
`The artifacts were removed ${formattedDate}`,
);
+
+ expect(
+ findArtifactRemoveElt()
+ .find('[data-testid="artifact-expired-help-link"]')
+ .attributes('href'),
+ ).toBe('help-url');
});
it('does not show the keep button', () => {
@@ -94,6 +103,12 @@ describe('Artifacts block', () => {
expect(trimText(findArtifactRemoveElt().text())).toBe(
`The artifacts will be removed ${formattedDate}`,
);
+
+ expect(
+ findArtifactRemoveElt()
+ .find('[data-testid="artifact-expired-help-link"]')
+ .attributes('href'),
+ ).toBe('help-url');
});
it('renders the keep button', () => {
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index e9ecafcd4c3..94653d4d4c7 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -33,6 +33,7 @@ describe('Job App', () => {
};
const props = {
+ artifactHelpUrl: 'help/artifact',
runnerHelpUrl: 'help/runner',
deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners',
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index bf2f8c05806..66f22162c97 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -35,7 +35,7 @@ describe('Job Log Collapsible Section', () => {
});
it('renders an icon with the closed state', () => {
- expect(findCollapsibleLineSvg().classes()).toContain('ic-angle-right');
+ expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('angle-right-icon');
});
});
@@ -52,7 +52,7 @@ describe('Job Log Collapsible Section', () => {
});
it('renders an icon with the open state', () => {
- expect(findCollapsibleLineSvg().classes()).toContain('ic-angle-down');
+ expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('angle-down-icon');
});
it('renders collapsible lines content', () => {
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index 5ce69221dab..bb90949b1f4 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -38,7 +38,7 @@ describe('Job Log Header Line', () => {
});
it('renders the line number component', () => {
- expect(wrapper.contains(LineNumber)).toBe(true);
+ expect(wrapper.find(LineNumber).exists()).toBe(true);
});
it('renders a span the provided text', () => {
@@ -90,7 +90,7 @@ describe('Job Log Header Line', () => {
});
it('renders the duration badge', () => {
- expect(wrapper.contains(DurationBadge)).toBe(true);
+ expect(wrapper.find(DurationBadge).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/jobs/components/log/line_spec.js
index ec3a3968f14..c2412a807c3 100644
--- a/spec/frontend/jobs/components/log/line_spec.js
+++ b/spec/frontend/jobs/components/log/line_spec.js
@@ -35,7 +35,7 @@ describe('Job Log Line', () => {
});
it('renders the line number component', () => {
- expect(wrapper.contains(LineNumber)).toBe(true);
+ expect(wrapper.find(LineNumber).exists()).toBe(true);
});
it('renders a span the provided text', () => {
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index 02cdb31d27e..015d5e01a46 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -42,6 +42,8 @@ describe('Job Log', () => {
wrapper.destroy();
});
+ const findCollapsibleLine = () => wrapper.find('.collapsible-line');
+
describe('line numbers', () => {
it('renders a line number for each open line', () => {
expect(wrapper.find('#L1').text()).toBe('1');
@@ -56,18 +58,22 @@ describe('Job Log', () => {
describe('collapsible sections', () => {
it('renders a clickable header section', () => {
- expect(wrapper.find('.collapsible-line').attributes('role')).toBe('button');
+ expect(findCollapsibleLine().attributes('role')).toBe('button');
});
it('renders an icon with the open state', () => {
- expect(wrapper.find('.collapsible-line svg').classes()).toContain('ic-angle-down');
+ expect(
+ findCollapsibleLine()
+ .find('[data-testid="angle-down-icon"]')
+ .exists(),
+ ).toBe(true);
});
describe('on click header section', () => {
it('calls toggleCollapsibleLine', () => {
jest.spyOn(wrapper.vm, 'toggleCollapsibleLine');
- wrapper.find('.collapsible-line').trigger('click');
+ findCollapsibleLine().trigger('click');
expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled();
});
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index 82fd73ef033..547f146cf88 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import Form from '~/jobs/components/manual_variables_form.vue';
const localVue = createLocalVue();
@@ -95,7 +95,7 @@ describe('Manual Variables Form', () => {
});
it('removes the variable row', () => {
- wrapper.find(GlDeprecatedButton).vm.$emit('click');
+ wrapper.find(GlButton).vm.$emit('click');
expect(wrapper.vm.variables.length).toBe(0);
});
diff --git a/spec/frontend/jobs/store/helpers.js b/spec/frontend/jobs/store/helpers.js
index 81a769b4a6e..78e33394b63 100644
--- a/spec/frontend/jobs/store/helpers.js
+++ b/spec/frontend/jobs/store/helpers.js
@@ -1,6 +1,5 @@
import state from '~/jobs/store/state';
-// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
store.replaceState(state());
};
diff --git a/spec/frontend/labels_issue_sidebar_spec.js b/spec/frontend/labels_issue_sidebar_spec.js
index fafefca94df..f74547c0554 100644
--- a/spec/frontend/labels_issue_sidebar_spec.js
+++ b/spec/frontend/labels_issue_sidebar_spec.js
@@ -7,7 +7,6 @@ import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
-import '~/gl_dropdown';
import 'select2';
import '~/api';
import '~/create_label';
diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js
new file mode 100644
index 00000000000..e804cae7914
--- /dev/null
+++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js
@@ -0,0 +1,131 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import setupAxiosStartupCalls from '~/lib/utils/axios_startup_calls';
+
+describe('setupAxiosStartupCalls', () => {
+ const AXIOS_RESPONSE = { text: 'AXIOS_RESPONSE' };
+ const STARTUP_JS_RESPONSE = { text: 'STARTUP_JS_RESPONSE' };
+ let mock;
+
+ function mockFetchCall(status) {
+ const p = {
+ ok: status >= 200 && status < 300,
+ status,
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ statusText: `MOCK-FETCH ${status}`,
+ clone: () => p,
+ json: () => Promise.resolve(STARTUP_JS_RESPONSE),
+ };
+ return Promise.resolve(p);
+ }
+
+ function mockConsoleWarn() {
+ jest.spyOn(console, 'warn').mockImplementation();
+ }
+
+ function expectConsoleWarn(path) {
+ // eslint-disable-next-line no-console
+ expect(console.warn).toHaveBeenCalledWith(expect.stringMatching(path), expect.any(Error));
+ }
+
+ beforeEach(() => {
+ window.gl = {};
+ mock = new MockAdapter(axios);
+ mock.onGet('/non-startup').reply(200, AXIOS_RESPONSE);
+ mock.onGet('/startup').reply(200, AXIOS_RESPONSE);
+ mock.onGet('/startup-failing').reply(200, AXIOS_RESPONSE);
+ });
+
+ afterEach(() => {
+ delete window.gl;
+ axios.interceptors.request.handlers = [];
+ mock.restore();
+ });
+
+ it('if no startupCalls are registered: does not register a request interceptor', () => {
+ setupAxiosStartupCalls(axios);
+
+ expect(axios.interceptors.request.handlers.length).toBe(0);
+ });
+
+ describe('if startupCalls are registered', () => {
+ beforeEach(() => {
+ window.gl.startup_calls = {
+ '/startup': {
+ fetchCall: mockFetchCall(200),
+ },
+ '/startup-failing': {
+ fetchCall: mockFetchCall(400),
+ },
+ };
+ setupAxiosStartupCalls(axios);
+ });
+
+ it('registers a request interceptor', () => {
+ expect(axios.interceptors.request.handlers.length).toBe(1);
+ });
+
+ it('detaches the request interceptor if every startup call has been made', async () => {
+ expect(axios.interceptors.request.handlers[0]).not.toBeNull();
+
+ await axios.get('/startup');
+ mockConsoleWarn();
+ await axios.get('/startup-failing');
+
+ // Axios sets the interceptor to null
+ expect(axios.interceptors.request.handlers[0]).toBeNull();
+ });
+
+ it('delegates to startup calls if URL is registered and call is successful', async () => {
+ const { headers, data, status, statusText } = await axios.get('/startup');
+
+ expect(headers).toEqual({ 'content-type': 'application/json' });
+ expect(status).toBe(200);
+ expect(statusText).toBe('MOCK-FETCH 200');
+ expect(data).toEqual(STARTUP_JS_RESPONSE);
+ expect(data).not.toEqual(AXIOS_RESPONSE);
+ });
+
+ it('delegates to startup calls exactly once', async () => {
+ await axios.get('/startup');
+ const { data } = await axios.get('/startup');
+
+ expect(data).not.toEqual(STARTUP_JS_RESPONSE);
+ expect(data).toEqual(AXIOS_RESPONSE);
+ });
+
+ it('does not delegate to startup calls if the call is failing', async () => {
+ mockConsoleWarn();
+ const { data } = await axios.get('/startup-failing');
+
+ expect(data).not.toEqual(STARTUP_JS_RESPONSE);
+ expect(data).toEqual(AXIOS_RESPONSE);
+ expectConsoleWarn('/startup-failing');
+ });
+
+ it('does not delegate to startup call if URL is not registered', async () => {
+ const { data } = await axios.get('/non-startup');
+
+ expect(data).toEqual(AXIOS_RESPONSE);
+ expect(data).not.toEqual(STARTUP_JS_RESPONSE);
+ });
+ });
+
+ it('removes GitLab Base URL from startup call', async () => {
+ const oldGon = window.gon;
+ window.gon = { gitlab_url: 'https://example.org/gitlab' };
+
+ window.gl.startup_calls = {
+ '/startup': {
+ fetchCall: mockFetchCall(200),
+ },
+ };
+ setupAxiosStartupCalls(axios);
+
+ const { data } = await axios.get('https://example.org/gitlab/startup');
+
+ expect(data).toEqual(STARTUP_JS_RESPONSE);
+
+ window.gon = oldGon;
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 9eb5587e83c..5b1fdea058b 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -653,3 +653,17 @@ describe('differenceInSeconds', () => {
expect(datetimeUtility.differenceInSeconds(startDate, endDate)).toBe(expected);
});
});
+
+describe('differenceInMilliseconds', () => {
+ const startDateTime = new Date('2019-07-17T00:00:00.000Z');
+
+ it.each`
+ startDate | endDate | expected
+ ${startDateTime.getTime()} | ${new Date('2019-07-17T00:00:00.000Z')} | ${0}
+ ${startDateTime} | ${new Date('2019-07-17T12:00:00.000Z').getTime()} | ${43200000}
+ ${startDateTime} | ${new Date('2019-07-18T00:00:00.000Z').getTime()} | ${86400000}
+ ${new Date('2019-07-18T00:00:00.000Z')} | ${startDateTime.getTime()} | ${-86400000}
+ `('returns $expected for $endDate - $startDate', ({ startDate, endDate, expected }) => {
+ expect(datetimeUtility.differenceInMilliseconds(startDate, endDate)).toBe(expected);
+ });
+});
diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js
index 07ba7c29dfc..a69be99ab98 100644
--- a/spec/frontend/lib/utils/forms_spec.js
+++ b/spec/frontend/lib/utils/forms_spec.js
@@ -1,4 +1,4 @@
-import { serializeForm } from '~/lib/utils/forms';
+import { serializeForm, serializeFormObject, isEmptyValue } from '~/lib/utils/forms';
describe('lib/utils/forms', () => {
const createDummyForm = inputs => {
@@ -93,4 +93,46 @@ describe('lib/utils/forms', () => {
});
});
});
+
+ describe('isEmptyValue', () => {
+ it.each`
+ input | returnValue
+ ${''} | ${true}
+ ${[]} | ${true}
+ ${null} | ${true}
+ ${undefined} | ${true}
+ ${'hello'} | ${false}
+ ${' '} | ${false}
+ ${0} | ${false}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(isEmptyValue(input)).toBe(returnValue);
+ });
+ });
+
+ describe('serializeFormObject', () => {
+ it('returns an serialized object', () => {
+ const form = {
+ profileName: { value: 'hello', state: null, feedback: null },
+ spiderTimeout: { value: 2, state: true, feedback: null },
+ targetTimeout: { value: 12, state: true, feedback: null },
+ };
+ expect(serializeFormObject(form)).toEqual({
+ profileName: 'hello',
+ spiderTimeout: 2,
+ targetTimeout: 12,
+ });
+ });
+
+ it('returns only the entries with value', () => {
+ const form = {
+ profileName: { value: '', state: null, feedback: null },
+ spiderTimeout: { value: 0, state: null, feedback: null },
+ targetTimeout: { value: null, state: null, feedback: null },
+ name: { value: undefined, state: null, feedback: null },
+ };
+ expect(serializeFormObject(form)).toEqual({
+ spiderTimeout: 0,
+ });
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 2e52958a828..1aaae80dcdf 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -1,4 +1,4 @@
-import { insertMarkdownText } from '~/lib/utils/text_markdown';
+import { insertMarkdownText, keypressNoteText } from '~/lib/utils/text_markdown';
describe('init markdown', () => {
let textArea;
@@ -115,14 +115,15 @@ describe('init markdown', () => {
describe('with selection', () => {
const text = 'initial selected value';
const selected = 'selected';
+ let selectedIndex;
+
beforeEach(() => {
textArea.value = text;
- const selectedIndex = text.indexOf(selected);
+ selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
});
it('applies the tag to the selected value', () => {
- const selectedIndex = text.indexOf(selected);
const tag = '*';
insertMarkdownText({
@@ -153,6 +154,29 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`));
});
+ it.each`
+ key | expected
+ ${'['} | ${`[${selected}]`}
+ ${'*'} | ${`**${selected}**`}
+ ${"'"} | ${`'${selected}'`}
+ ${'_'} | ${`_${selected}_`}
+ ${'`'} | ${`\`${selected}\``}
+ ${'"'} | ${`"${selected}"`}
+ ${'{'} | ${`{${selected}}`}
+ ${'('} | ${`(${selected})`}
+ ${'<'} | ${`<${selected}>`}
+ `('generates $expected when $key is pressed', ({ key, expected }) => {
+ const event = new KeyboardEvent('keydown', { key });
+
+ textArea.addEventListener('keydown', keypressNoteText);
+ textArea.dispatchEvent(event);
+
+ expect(textArea.value).toEqual(text.replace(selected, expected));
+
+ // cursor placement should be after selection + 2 tag lengths
+ expect(textArea.selectionStart).toBe(selectedIndex + expected.length);
+ });
+
describe('and text to be selected', () => {
const tag = '[{text}](url)';
const select = 'url';
@@ -178,7 +202,7 @@ describe('init markdown', () => {
it('selects the right text when multiple tags are present', () => {
const initialValue = `${tag} ${tag} ${selected}`;
textArea.value = initialValue;
- const selectedIndex = initialValue.indexOf(selected);
+ selectedIndex = initialValue.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({
textArea,
@@ -204,7 +228,7 @@ describe('init markdown', () => {
const initialValue = `text ${expectedUrl} text`;
textArea.value = initialValue;
- const selectedIndex = initialValue.indexOf(expectedUrl);
+ selectedIndex = initialValue.indexOf(expectedUrl);
textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length);
insertMarkdownText({
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 285f7d04c3b..6fef5f6b63c 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -205,6 +205,27 @@ describe('text_utility', () => {
});
});
+ describe('convertUnicodeToAscii', () => {
+ it('does nothing on an empty string', () => {
+ expect(textUtils.convertUnicodeToAscii('')).toBe('');
+ });
+
+ it('does nothing on an already ascii string', () => {
+ expect(textUtils.convertUnicodeToAscii('The quick brown fox jumps over the lazy dog.')).toBe(
+ 'The quick brown fox jumps over the lazy dog.',
+ );
+ });
+
+ it('replaces Unicode characters', () => {
+ expect(textUtils.convertUnicodeToAscii('Dĭd söméònê äšk fœŕ Ůnĭċődę?')).toBe(
+ 'Did soemeone aesk foer Unicode?',
+ );
+
+ expect(textUtils.convertUnicodeToAscii("Jürgen's Projekt")).toBe("Juergen's Projekt");
+ expect(textUtils.convertUnicodeToAscii('öäüÖÄÜ')).toBe('oeaeueOeAeUe');
+ });
+ });
+
describe('splitCamelCase', () => {
it('separates a PascalCase word to two', () => {
expect(textUtils.splitCamelCase('HelloWorld')).toBe('Hello World');
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index a13ac3778cf..869ae274a3f 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -161,6 +161,15 @@ describe('URL utility', () => {
);
});
+ it('sorts params in alphabetical order with sort option', () => {
+ expect(mergeUrlParams({ c: 'c', b: 'b', a: 'a' }, 'https://host/path', { sort: true })).toBe(
+ 'https://host/path?a=a&b=b&c=c',
+ );
+ expect(
+ mergeUrlParams({ alpha: 'alpha' }, 'https://host/path?op=/&foo=bar', { sort: true }),
+ ).toBe('https://host/path?alpha=alpha&foo=bar&op=%2F');
+ });
+
describe('with spread array option', () => {
const spreadArrayOptions = { spreadArrays: true };
@@ -616,6 +625,35 @@ describe('URL utility', () => {
expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' });
});
+
+ describe('with gatherArrays=false', () => {
+ it('overwrites values with the same array-key and does not change the key', () => {
+ const searchQuery = '?one[]=1&one[]=2&two=2&two=3';
+
+ expect(urlUtils.queryToObject(searchQuery)).toEqual({ 'one[]': '2', two: '3' });
+ });
+ });
+
+ describe('with gatherArrays=true', () => {
+ const options = { gatherArrays: true };
+ it('gathers only values with the same array-key and strips `[]` from the key', () => {
+ const searchQuery = '?one[]=1&one[]=2&two=2&two=3';
+
+ expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: ['1', '2'], two: '3' });
+ });
+
+ it('overwrites values with the same array-key name', () => {
+ const searchQuery = '?one=1&one[]=2&two=2&two=3';
+
+ expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: ['2'], two: '3' });
+ });
+
+ it('overwrites values with the same key name', () => {
+ const searchQuery = '?one[]=1&one=2&two=2&two=3';
+
+ expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: '2', two: '3' });
+ });
+ });
});
describe('objectToQuery', () => {
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index 6421aca684f..559ce4f9414 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -39,13 +39,22 @@ describe('EnvironmentLogs', () => {
};
const updateControlBtnsMock = jest.fn();
+ const LogControlButtonsStub = {
+ template: '<div/>',
+ methods: {
+ update: updateControlBtnsMock,
+ },
+ props: {
+ scrollDownButtonDisabled: false,
+ },
+ };
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
const findSimpleFilters = () => wrapper.find({ ref: 'log-simple-filters' });
const findAdvancedFilters = () => wrapper.find({ ref: 'log-advanced-filters' });
const findElasticsearchNotice = () => wrapper.find({ ref: 'elasticsearchNotice' });
- const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
+ const findLogControlButtons = () => wrapper.find(LogControlButtonsStub);
const findInfiniteScroll = () => wrapper.find({ ref: 'infiniteScroll' });
const findLogTrace = () => wrapper.find({ ref: 'logTrace' });
@@ -76,16 +85,7 @@ describe('EnvironmentLogs', () => {
propsData,
store,
stubs: {
- LogControlButtons: {
- name: 'log-control-buttons-stub',
- template: '<div/>',
- methods: {
- update: updateControlBtnsMock,
- },
- props: {
- scrollDownButtonDisabled: false,
- },
- },
+ LogControlButtons: LogControlButtonsStub,
GlInfiniteScroll: {
name: 'gl-infinite-scroll',
template: `
@@ -121,9 +121,6 @@ describe('EnvironmentLogs', () => {
it('displays UI elements', () => {
initWrapper();
- expect(wrapper.isVueInstance()).toBe(true);
- expect(wrapper.isEmpty()).toBe(false);
-
expect(findEnvironmentsDropdown().is(GlDeprecatedDropdown)).toBe(true);
expect(findSimpleFilters().exists()).toBe(true);
expect(findLogControlButtons().exists()).toBe(true);
diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js
index 007c5000e16..3a3c23c95b8 100644
--- a/spec/frontend/logs/components/log_advanced_filters_spec.js
+++ b/spec/frontend/logs/components/log_advanced_filters_spec.js
@@ -68,9 +68,6 @@ describe('LogAdvancedFilters', () => {
it('displays UI elements', () => {
initWrapper();
- expect(wrapper.isVueInstance()).toBe(true);
- expect(wrapper.isEmpty()).toBe(false);
-
expect(findFilteredSearch().exists()).toBe(true);
expect(findTimeRangePicker().exists()).toBe(true);
});
diff --git a/spec/frontend/logs/components/log_control_buttons_spec.js b/spec/frontend/logs/components/log_control_buttons_spec.js
index 38e568f569f..dff38ecb15e 100644
--- a/spec/frontend/logs/components/log_control_buttons_spec.js
+++ b/spec/frontend/logs/components/log_control_buttons_spec.js
@@ -28,9 +28,6 @@ describe('LogControlButtons', () => {
it('displays UI elements', () => {
initWrapper();
- expect(wrapper.isVueInstance()).toBe(true);
- expect(wrapper.isEmpty()).toBe(false);
-
expect(findScrollToTop().is(GlButton)).toBe(true);
expect(findScrollToBottom().is(GlButton)).toBe(true);
expect(findRefreshBtn().is(GlButton)).toBe(true);
@@ -57,7 +54,7 @@ describe('LogControlButtons', () => {
});
it('click on "scroll to top" scrolls up', () => {
- expect(findScrollToTop().is('[disabled]')).toBe(false);
+ expect(findScrollToTop().attributes('disabled')).toBeUndefined();
findScrollToTop().vm.$emit('click');
@@ -65,7 +62,7 @@ describe('LogControlButtons', () => {
});
it('click on "scroll to bottom" scrolls down', () => {
- expect(findScrollToBottom().is('[disabled]')).toBe(false);
+ expect(findScrollToBottom().attributes('disabled')).toBeUndefined();
findScrollToBottom().vm.$emit('click');
diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js
index e739621431e..1e30a7df559 100644
--- a/spec/frontend/logs/components/log_simple_filters_spec.js
+++ b/spec/frontend/logs/components/log_simple_filters_spec.js
@@ -18,7 +18,7 @@ describe('LogSimpleFilters', () => {
const findPodsDropdownItems = () =>
findPodsDropdown()
.findAll(GlDeprecatedDropdownItem)
- .filter(item => !item.is('[disabled]'));
+ .filter(item => !('disabled' in item.attributes()));
const mockPodsLoading = () => {
state.pods.options = [];
@@ -59,9 +59,6 @@ describe('LogSimpleFilters', () => {
it('displays UI elements', () => {
initWrapper();
- expect(wrapper.isVueInstance()).toBe(true);
- expect(wrapper.isEmpty()).toBe(false);
-
expect(findPodsDropdown().exists()).toBe(true);
});
diff --git a/spec/frontend/logs/mock_data.js b/spec/frontend/logs/mock_data.js
index f4c567a2ea3..3fabab4bc59 100644
--- a/spec/frontend/logs/mock_data.js
+++ b/spec/frontend/logs/mock_data.js
@@ -35,6 +35,7 @@ export const mockManagedApps = [
status: 'connected',
path: '/root/autodevops-deploy/-/clusters/15',
gitlab_managed_apps_logs_path: '/root/autodevops-deploy/-/logs?cluster_id=15',
+ enable_advanced_logs_querying: true,
},
{
cluster_type: 'project_type',
@@ -45,6 +46,7 @@ export const mockManagedApps = [
status: 'connected',
path: '/root/autodevops-deploy/-/clusters/16',
gitlab_managed_apps_logs_path: null,
+ enable_advanced_logs_querying: false,
},
];
diff --git a/spec/frontend/logs/stores/getters_spec.js b/spec/frontend/logs/stores/getters_spec.js
index 9d213d8c01f..bca1ce4ca92 100644
--- a/spec/frontend/logs/stores/getters_spec.js
+++ b/spec/frontend/logs/stores/getters_spec.js
@@ -1,7 +1,14 @@
import { trace, showAdvancedFilters } from '~/logs/stores/getters';
import logsPageState from '~/logs/stores/state';
-import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data';
+import {
+ mockLogsResult,
+ mockTrace,
+ mockEnvName,
+ mockEnvironments,
+ mockManagedApps,
+ mockManagedAppName,
+} from '../mock_data';
describe('Logs Store getters', () => {
let state;
@@ -72,4 +79,43 @@ describe('Logs Store getters', () => {
});
});
});
+
+ describe('when no managedApps are set', () => {
+ beforeEach(() => {
+ state.environments.current = null;
+ state.environments.options = [];
+ state.managedApps.current = mockManagedAppName;
+ state.managedApps.options = [];
+ });
+
+ it('returns false', () => {
+ expect(showAdvancedFilters(state)).toBe(false);
+ });
+ });
+
+ describe('when the managedApp supports filters', () => {
+ beforeEach(() => {
+ state.environments.current = null;
+ state.environments.options = mockEnvironments;
+ state.managedApps.current = mockManagedAppName;
+ state.managedApps.options = mockManagedApps;
+ });
+
+ it('returns true', () => {
+ expect(showAdvancedFilters(state)).toBe(true);
+ });
+ });
+
+ describe('when the managedApp does not support filters', () => {
+ beforeEach(() => {
+ state.environments.current = null;
+ state.environments.options = mockEnvironments;
+ state.managedApps.options = mockManagedApps;
+ state.managedApps.current = mockManagedApps[1].name;
+ });
+
+ it('returns false', () => {
+ expect(showAdvancedFilters(state)).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/matchers.js b/spec/frontend/matchers.js
index 53c6a72eea0..50feba86a61 100644
--- a/spec/frontend/matchers.js
+++ b/spec/frontend/matchers.js
@@ -9,8 +9,8 @@ export default {
}
const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
- const matchingIcon = iconReferences.find(reference =>
- reference.getAttribute('xlink:href').endsWith(`#${iconName}`),
+ const matchingIcon = iconReferences.find(
+ reference => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
);
const pass = Boolean(matchingIcon);
@@ -22,7 +22,7 @@ export default {
message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
const existingIcons = iconReferences.map(reference => {
- const iconUrl = reference.getAttribute('xlink:href');
+ const iconUrl = reference.getAttribute('href');
return `"${iconUrl.replace(/^.+#/, '')}"`;
});
if (existingIcons.length > 0) {
diff --git a/spec/frontend/milestones/project_milestone_combobox_spec.js b/spec/frontend/milestones/project_milestone_combobox_spec.js
index 2265c9bdc2e..60d68aa5816 100644
--- a/spec/frontend/milestones/project_milestone_combobox_spec.js
+++ b/spec/frontend/milestones/project_milestone_combobox_spec.js
@@ -1,11 +1,13 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
-import { GlNewDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
import { milestones as projectMilestones } from './mock_data';
const TEST_SEARCH_ENDPOINT = '/api/v4/projects/8/search';
+const TEST_SEARCH = 'TEST_SEARCH';
const extraLinks = [
{ text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
@@ -21,6 +23,8 @@ describe('Milestone selector', () => {
const findNoResultsMessage = () => wrapper.find({ ref: 'noResults' });
+ const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+
const factory = (options = {}) => {
wrapper = shallowMount(MilestoneCombobox, {
...options,
@@ -49,7 +53,7 @@ describe('Milestone selector', () => {
});
it('renders the dropdown', () => {
- expect(wrapper.find(GlNewDropdown)).toExist();
+ expect(wrapper.find(GlDropdown)).toExist();
});
it('renders additional links', () => {
@@ -63,7 +67,7 @@ describe('Milestone selector', () => {
describe('before results', () => {
it('should show a loading icon', () => {
const request = mock.onGet(TEST_SEARCH_ENDPOINT, {
- params: { search: 'TEST_SEARCH', scope: 'milestones' },
+ params: { search: TEST_SEARCH, scope: 'milestones' },
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
@@ -85,9 +89,9 @@ describe('Milestone selector', () => {
describe('with empty results', () => {
beforeEach(() => {
mock
- .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
+ .onGet(TEST_SEARCH_ENDPOINT, { params: { search: TEST_SEARCH, scope: 'milestones' } })
.reply(200, []);
- wrapper.find(GlSearchBoxByType).vm.$emit('input', 'TEST_SEARCH');
+ findSearchBox().vm.$emit('input', TEST_SEARCH);
return axios.waitForAll();
});
@@ -116,7 +120,7 @@ describe('Milestone selector', () => {
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
},
]);
- wrapper.find(GlSearchBoxByType).vm.$emit('input', 'v0.1');
+ findSearchBox().vm.$emit('input', 'v0.1');
return axios.waitForAll().then(() => {
items = wrapper.findAll('[role="milestone option"]');
});
@@ -147,4 +151,36 @@ describe('Milestone selector', () => {
expect(findNoResultsMessage().exists()).toBe(false);
});
});
+
+ describe('when Enter is pressed', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ projectId,
+ preselectedMilestones,
+ extraLinks,
+ },
+ data() {
+ return {
+ searchQuery: 'TEST_SEARCH',
+ };
+ },
+ });
+
+ mock
+ .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
+ .reply(200, []);
+ });
+
+ it('should trigger a search', async () => {
+ mock.resetHistory();
+
+ findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].url).toBe(TEST_SEARCH_ENDPOINT);
+ });
+ });
});
diff --git a/spec/frontend/mocks/mocks_helper.js b/spec/frontend/mocks/mocks_helper.js
index 823ab41a5ba..0aa80331434 100644
--- a/spec/frontend/mocks/mocks_helper.js
+++ b/spec/frontend/mocks/mocks_helper.js
@@ -29,7 +29,6 @@ const getMockFiles = root => readdir.sync(root, { deep: MAX_DEPTH, filter: mockF
const defaultSetMock = (srcPath, mockPath) =>
jest.mock(srcPath, () => jest.requireActual(mockPath));
-// eslint-disable-next-line import/prefer-default-export
export const setupManualMocks = function setupManualMocks(setMock = defaultSetMock) {
prefixMap.forEach(({ mocksRoot, requirePrefix }) => {
const mocksRootAbsolute = path.join(__dirname, mocksRoot);
diff --git a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
index 59c17daacff..2a8ce1d3f30 100644
--- a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
+++ b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
@@ -13,7 +13,7 @@ exports[`AlertWidget Alert firing displays a warning icon and matches snapshot 1
/>
<span
- class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"
+ class="text-truncate gl-pl-2"
>
Firing:
alert-label &gt; 42
@@ -35,7 +35,7 @@ exports[`AlertWidget Alert not firing displays a warning icon and matches snapsh
/>
<span
- class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"
+ class="text-truncate gl-pl-2"
>
alert-label &gt; 42
</span>
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
index 193dbb3e63f..d004b1da0b6 100644
--- a/spec/frontend/monitoring/alert_widget_spec.js
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -84,7 +84,7 @@ describe('AlertWidget', () => {
},
});
};
- const hasLoadingIcon = () => wrapper.contains(GlLoadingIcon);
+ const hasLoadingIcon = () => wrapper.find(GlLoadingIcon).exists();
const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' });
const findAlertErrorMessage = () => wrapper.find({ ref: 'alertErrorMessage' });
const findCurrentSettingsText = () =>
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 7ef956f8e05..a28ecac00fd 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -32,7 +32,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
- <gl-new-dropdown-stub
+ <gl-dropdown-stub
category="tertiary"
class="flex-grow-1"
data-qa-selector="environments_dropdown"
@@ -47,12 +47,12 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="d-flex flex-column overflow-hidden"
>
- <gl-new-dropdown-header-stub>
+ <gl-dropdown-section-header-stub>
Environment
- </gl-new-dropdown-header-stub>
+ </gl-dropdown-section-header-stub>
<gl-search-box-by-type-stub
- class="m-2"
+ class="gl-m-3"
clearbuttontitle="Clear"
value=""
/>
@@ -69,7 +69,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
- </gl-new-dropdown-stub>
+ </gl-dropdown-stub>
</div>
<div
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index 15a52d03bcd..ebb49a2a0aa 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -46,9 +46,8 @@ describe('Anomaly chart component', () => {
});
});
- it('is a Vue instance', () => {
+ it('renders correctly', () => {
expect(findTimeSeries().exists()).toBe(true);
- expect(findTimeSeries().isVueInstance()).toBe(true);
});
describe('receives props correctly', () => {
diff --git a/spec/frontend/monitoring/components/charts/bar_spec.js b/spec/frontend/monitoring/components/charts/bar_spec.js
index e39e6e7e2c2..a363fafdc31 100644
--- a/spec/frontend/monitoring/components/charts/bar_spec.js
+++ b/spec/frontend/monitoring/components/charts/bar_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlBarChart } from '@gitlab/ui/dist/charts';
import Bar from '~/monitoring/components/charts/bar.vue';
-import { barMockData } from '../../mock_data';
+import { barGraphData } from '../../graph_data';
jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
@@ -10,11 +10,14 @@ jest.mock('~/lib/utils/icon_utils', () => ({
describe('Bar component', () => {
let barChart;
let store;
+ let graphData;
beforeEach(() => {
+ graphData = barGraphData();
+
barChart = shallowMount(Bar, {
propsData: {
- graphData: barMockData,
+ graphData,
},
store,
});
@@ -31,15 +34,11 @@ describe('Bar component', () => {
beforeEach(() => {
glbarChart = barChart.find(GlBarChart);
- chartData = barChart.vm.chartData[barMockData.metrics[0].label];
- });
-
- it('is a Vue instance', () => {
- expect(glbarChart.isVueInstance()).toBe(true);
+ chartData = barChart.vm.chartData[graphData.metrics[0].label];
});
it('should display a label on the x axis', () => {
- expect(glbarChart.vm.xAxisTitle).toBe(barMockData.xLabel);
+ expect(glbarChart.props('xAxisTitle')).toBe(graphData.xLabel);
});
it('should return chartData as array of arrays', () => {
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
index a2056d96dcf..16e2080c000 100644
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ b/spec/frontend/monitoring/components/charts/column_spec.js
@@ -95,10 +95,6 @@ describe('Column component', () => {
describe('wrapped components', () => {
describe('GitLab UI column chart', () => {
- it('is a Vue instance', () => {
- expect(findChart().isVueInstance()).toBe(true);
- });
-
it('receives data properties needed for proper chart render', () => {
expect(chartProps('data').values).toEqual(dataValues);
});
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
index 27a2021e9be..c8375810a7b 100644
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js
@@ -24,21 +24,14 @@ describe('Heatmap component', () => {
};
describe('wrapped chart', () => {
- let glHeatmapChart;
-
beforeEach(() => {
createWrapper();
- glHeatmapChart = findChart();
});
afterEach(() => {
wrapper.destroy();
});
- it('is a Vue instance', () => {
- expect(glHeatmapChart.isVueInstance()).toBe(true);
- });
-
it('should display a label on the x axis', () => {
expect(wrapper.vm.xAxisName).toBe(graphData.xLabel);
});
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
index bb2fbc68eaa..24a2af87eb8 100644
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
@@ -3,13 +3,15 @@ import timezoneMock from 'timezone-mock';
import { cloneDeep } from 'lodash';
import { GlStackedColumnChart, GlChartLegend } from '@gitlab/ui/dist/charts';
import StackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
-import { stackedColumnMockedData } from '../../mock_data';
+import { stackedColumnGraphData } from '../../graph_data';
jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockImplementation(icon => Promise.resolve(`${icon}-content`)),
}));
describe('Stacked column chart component', () => {
+ const stackedColumnMockedData = stackedColumnGraphData();
+
let wrapper;
const findChart = () => wrapper.find(GlStackedColumnChart);
@@ -63,9 +65,9 @@ describe('Stacked column chart component', () => {
const groupBy = findChart().props('groupBy');
expect(groupBy).toEqual([
- '2020-01-30T12:00:00.000Z',
- '2020-01-30T12:01:00.000Z',
- '2020-01-30T12:02:00.000Z',
+ '2015-07-01T20:10:50.000Z',
+ '2015-07-01T20:12:50.000Z',
+ '2015-07-01T20:14:50.000Z',
]);
});
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 6f9a89feb3e..7f0ff534db3 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -632,9 +632,8 @@ describe('Time series component', () => {
return wrapper.vm.$nextTick();
});
- it('is a Vue instance', () => {
+ it('exists', () => {
expect(findChartComponent().exists()).toBe(true);
- expect(findChartComponent().isVueInstance()).toBe(true);
});
it('receives data properties needed for proper chart render', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index 024b2cbd7f1..b22e05ec30a 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlNewDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import { createStore } from '~/monitoring/stores';
import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
import { setupAllDashboards, setupStoreWithData } from '../store_utils';
@@ -146,8 +146,8 @@ describe('Actions menu', () => {
});
describe('add panel item', () => {
- const GlNewDropdownItemStub = {
- extends: GlNewDropdownItem,
+ const GlDropdownItemStub = {
+ extends: GlDropdownItem,
props: {
to: [String, Object],
},
@@ -164,7 +164,7 @@ describe('Actions menu', () => {
},
{
mocks: { $route },
- stubs: { GlNewDropdownItem: GlNewDropdownItemStub },
+ stubs: { GlDropdownItem: GlDropdownItemStub },
},
);
});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 5cf24706ebd..f9a7a4d5a93 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlNewDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
+import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
@@ -31,7 +31,7 @@ describe('Dashboard header', () => {
const findDashboardDropdown = () => wrapper.find(DashboardsDropdown);
const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
- const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlNewDropdownItem);
+ const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlDropdownItem);
const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType);
const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' });
const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon);
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index 587ddd23d3f..08c69701bd2 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -68,7 +68,7 @@ describe('dashboard invalid url parameters', () => {
it('form exists and can be submitted', () => {
expect(findForm().exists()).toBe(true);
expect(findSubmitBtn().exists()).toBe(true);
- expect(findSubmitBtn().is('[disabled]')).toBe(false);
+ expect(findSubmitBtn().props('disabled')).toBe(false);
});
it('form has a text area with a default value', () => {
@@ -109,7 +109,7 @@ describe('dashboard invalid url parameters', () => {
});
it('submit button is disabled', () => {
- expect(findSubmitBtn().is('[disabled]')).toBe(true);
+ expect(findSubmitBtn().props('disabled')).toBe(true);
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index fb96bcc042f..8947a6c1570 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -2,7 +2,7 @@ import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { setTestTimeout } from 'helpers/timeout';
-import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
@@ -15,10 +15,14 @@ import {
mockNamespace,
mockNamespacedData,
mockTimeRange,
- barMockData,
} from '../mock_data';
import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
-import { anomalyGraphData, singleStatGraphData, heatmapGraphData } from '../graph_data';
+import {
+ anomalyGraphData,
+ singleStatGraphData,
+ heatmapGraphData,
+ barGraphData,
+} from '../graph_data';
import { panelTypes } from '~/monitoring/constants';
@@ -137,7 +141,6 @@ describe('Dashboard Panel', () => {
it('The Empty Chart component is rendered and is a Vue instance', () => {
expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
- expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
});
});
@@ -166,7 +169,6 @@ describe('Dashboard Panel', () => {
it('The Empty Chart component is rendered and is a Vue instance', () => {
expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
- expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
});
});
@@ -222,13 +224,11 @@ describe('Dashboard Panel', () => {
it('empty chart is rendered for empty results', () => {
createWrapper({ graphData: graphDataEmpty });
expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
- expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
});
it('area chart is rendered by default', () => {
createWrapper();
expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
- expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
});
describe.each`
@@ -240,7 +240,7 @@ describe('Dashboard Panel', () => {
${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false}
${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
${heatmapGraphData()} | ${MonitorHeatmapChart} | ${false}
- ${barMockData} | ${MonitorBarChart} | ${false}
+ ${barGraphData()} | ${MonitorBarChart} | ${false}
`('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
@@ -250,7 +250,6 @@ describe('Dashboard Panel', () => {
it(`renders the chart component and binds attributes`, () => {
expect(wrapper.find(component).exists()).toBe(true);
- expect(wrapper.find(component).isVueInstance()).toBe(true);
expect(wrapper.find(component).attributes()).toMatchObject(attrs);
});
@@ -544,7 +543,6 @@ describe('Dashboard Panel', () => {
});
it('it renders a time series chart with no errors', () => {
- expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index f37d95317ab..b7a0ea46b61 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -645,7 +645,7 @@ describe('Dashboard', () => {
it('it enables draggables', () => {
expect(findRearrangeButton().attributes('pressed')).toBeTruthy();
- expect(findEnabledDraggables()).toEqual(findDraggables());
+ expect(findEnabledDraggables().wrappers).toEqual(findDraggables().wrappers);
});
it('metrics can be swapped', () => {
@@ -668,7 +668,11 @@ describe('Dashboard', () => {
});
it('shows a remove button, which removes a panel', () => {
- expect(findFirstDraggableRemoveButton().isEmpty()).toBe(false);
+ expect(
+ findFirstDraggableRemoveButton()
+ .find('a')
+ .exists(),
+ ).toBe(true);
expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount);
findFirstDraggableRemoveButton().trigger('click');
@@ -703,8 +707,7 @@ describe('Dashboard', () => {
});
it('renders correctly', () => {
- expect(wrapper.isVueInstance()).toBe(true);
- expect(wrapper.exists()).toBe(true);
+ expect(wrapper.html()).not.toBe('');
});
});
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 89adbad386f..ef5784183b2 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlNewDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
@@ -33,8 +33,8 @@ describe('DashboardsDropdown', () => {
});
}
- const findItems = () => wrapper.findAll(GlNewDropdownItem);
- const findItemAt = i => wrapper.findAll(GlNewDropdownItem).at(i);
+ 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' });
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
index 29e4c4514fe..29115ffb817 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
@@ -50,7 +50,7 @@ describe('DuplicateDashboardForm', () => {
it('when is empty', () => {
setValue('fileName', '');
return wrapper.vm.$nextTick(() => {
- expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
+ expect(findByRef('fileNameFormGroup').classes()).toContain('is-valid');
expect(findInvalidFeedback().exists()).toBe(false);
});
});
@@ -58,7 +58,7 @@ describe('DuplicateDashboardForm', () => {
it('when is valid', () => {
setValue('fileName', 'my_dashboard.yml');
return wrapper.vm.$nextTick(() => {
- expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
+ expect(findByRef('fileNameFormGroup').classes()).toContain('is-valid');
expect(findInvalidFeedback().exists()).toBe(false);
});
});
@@ -66,7 +66,7 @@ describe('DuplicateDashboardForm', () => {
it('when is not valid', () => {
setValue('fileName', 'my_dashboard.exe');
return wrapper.vm.$nextTick(() => {
- expect(findByRef('fileNameFormGroup').is('.is-invalid')).toBe(true);
+ expect(findByRef('fileNameFormGroup').classes()).toContain('is-invalid');
expect(findInvalidFeedback().text()).toBeTruthy();
});
});
@@ -144,7 +144,7 @@ describe('DuplicateDashboardForm', () => {
return wrapper.vm.$nextTick().then(() => {
wrapper.find('form').trigger('change');
- expect(findByRef('branchName').is(':focus')).toBe(true);
+ expect(document.activeElement).toBe(findByRef('branchName').element);
});
});
});
diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
index 49c10483c45..b63995ec2d4 100644
--- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js
+++ b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
@@ -1,6 +1,6 @@
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlDeprecatedButton, GlCard } from '@gitlab/ui';
+import { GlButton, GlCard } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
@@ -71,16 +71,16 @@ describe('Embed Group', () => {
it('is expanded by default', () => {
metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
+ mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
expect(wrapper.find('.card-body').classes()).not.toContain('d-none');
});
it('collapses when clicked', done => {
metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
+ mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
- wrapper.find(GlDeprecatedButton).trigger('click');
+ wrapper.find(GlButton).trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.find('.card-body').classes()).toContain('d-none');
@@ -148,16 +148,16 @@ describe('Embed Group', () => {
describe('button text', () => {
it('has a singular label when there is one embed', () => {
metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
+ mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
- expect(wrapper.find(GlDeprecatedButton).text()).toBe('Hide chart');
+ expect(wrapper.find(GlButton).text()).toBe('Hide chart');
});
it('has a plural label when there are multiple embeds', () => {
metricsWithDataGetter.mockReturnValue([2]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } });
+ mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
- expect(wrapper.find(GlDeprecatedButton).text()).toBe('Hide charts');
+ expect(wrapper.find(GlButton).text()).toBe('Hide charts');
});
});
});
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 86e2523f708..ebcd6c0df3a 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -50,7 +50,7 @@ describe('Graph group component', () => {
it('should contain a tab index for the collapse button', () => {
const groupToggle = findToggleButton();
- expect(groupToggle.is('[tabindex]')).toBe(true);
+ expect(groupToggle.attributes('tabindex')).toBeDefined();
});
it('should show the open the group when collapseGroup is set to true', () => {
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index a9b8295f38e..8a478362b5e 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
-import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { createStore } from '~/monitoring/stores';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
@@ -15,8 +15,8 @@ describe('RefreshButton', () => {
};
const findRefreshBtn = () => wrapper.find(GlButton);
- const findDropdown = () => wrapper.find(GlNewDropdown);
- const findOptions = () => findDropdown().findAll(GlNewDropdownItem);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findOptions = () => findDropdown().findAll(GlDropdownItem);
const findOptionAt = index => findOptions().at(index);
const expectFetchDataToHaveBeenCalledTimes = times => {
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
index 30040d3f89f..18ec74550b4 100644
--- a/spec/frontend/monitoring/fixture_data.js
+++ b/spec/frontend/monitoring/fixture_data.js
@@ -1,8 +1,7 @@
import { stateAndPropsFromDataset } from '~/monitoring/utils';
import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
import { metricStates } from '~/monitoring/constants';
-import { convertObjectProps } from '~/lib/utils/common_utils';
-import { convertToCamelCase } from '~/lib/utils/text_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { metricsResult } from './mock_data';
@@ -14,13 +13,7 @@ export const metricsDashboardResponse = getJSONFixture(
export const metricsDashboardPayload = metricsDashboardResponse.dashboard;
const datasetState = stateAndPropsFromDataset(
- // It's preferable to have props in snake_case, this will be addressed at:
- // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33574
- convertObjectProps(
- // Some props use kebab-case, convert to snake_case first
- key => convertToCamelCase(key.replace(/-/g, '_')),
- metricsDashboardResponse.metrics_data,
- ),
+ convertObjectPropsToCamelCase(metricsDashboardResponse.metrics_data),
);
// new properties like addDashboardDocumentationPath prop and alertsEndpoint
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
index f85351e55d7..494fdb1b159 100644
--- a/spec/frontend/monitoring/graph_data.js
+++ b/spec/frontend/monitoring/graph_data.js
@@ -246,3 +246,29 @@ export const gaugeChartGraphData = (panelOptions = {}) => {
],
});
};
+
+/**
+ * Generates stacked mock graph data according to options
+ *
+ * @param {Object} panelOptions - Panel options as in YML.
+ * @param {Object} dataOptions
+ */
+export const stackedColumnGraphData = (panelOptions = {}, dataOptions = {}) => {
+ return {
+ ...timeSeriesGraphData(panelOptions, dataOptions),
+ type: panelTypes.STACKED_COLUMN,
+ };
+};
+
+/**
+ * Generates bar mock graph data according to options
+ *
+ * @param {Object} panelOptions - Panel options as in YML.
+ * @param {Object} dataOptions
+ */
+export const barGraphData = (panelOptions = {}, dataOptions = {}) => {
+ return {
+ ...timeSeriesGraphData(panelOptions, dataOptions),
+ type: panelTypes.BAR,
+ };
+};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 28a7dd1af4f..aea8815fb10 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -245,51 +245,6 @@ export const metricsResult = [
},
];
-export const stackedColumnMockedData = {
- title: 'memories',
- type: 'stacked-column',
- x_label: 'x label',
- y_label: 'y label',
- metrics: [
- {
- label: 'memory_1024',
- unit: 'count',
- series_name: 'group 1',
- prometheus_endpoint_path:
- '/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
- metricId: 'NO_DB_metric_of_ages_1024',
- result: [
- {
- metric: {},
- values: [
- ['2020-01-30T12:00:00.000Z', '5'],
- ['2020-01-30T12:01:00.000Z', '10'],
- ['2020-01-30T12:02:00.000Z', '15'],
- ],
- },
- ],
- },
- {
- label: 'memory_1000',
- unit: 'count',
- series_name: 'group 2',
- prometheus_endpoint_path:
- '/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
- metricId: 'NO_DB_metric_of_ages_1000',
- result: [
- {
- metric: {},
- values: [
- ['2020-01-30T12:00:00.000Z', '20'],
- ['2020-01-30T12:01:00.000Z', '25'],
- ['2020-01-30T12:02:00.000Z', '30'],
- ],
- },
- ],
- },
- ],
-};
-
export const barMockData = {
title: 'SLA Trends - Primary Services',
type: 'bar',
diff --git a/spec/frontend/mr_popover/mr_popover_spec.js b/spec/frontend/mr_popover/mr_popover_spec.js
index 3f62dca4a57..094d1a6472c 100644
--- a/spec/frontend/mr_popover/mr_popover_spec.js
+++ b/spec/frontend/mr_popover/mr_popover_spec.js
@@ -61,7 +61,7 @@ describe('MR Popover', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.contains(CiIcon)).toBe(false);
+ expect(wrapper.find(CiIcon).exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/namespace_select_spec.js b/spec/frontend/namespace_select_spec.js
index 399fa950769..d6f3eb75cd9 100644
--- a/spec/frontend/namespace_select_spec.js
+++ b/spec/frontend/namespace_select_spec.js
@@ -1,56 +1,55 @@
-import $ from 'jquery';
import NamespaceSelect from '~/namespace_select';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-describe('NamespaceSelect', () => {
- beforeEach(() => {
- jest.spyOn($.fn, 'glDropdown').mockImplementation(() => {});
- });
+jest.mock('~/deprecated_jquery_dropdown');
- it('initializes glDropdown', () => {
+describe('NamespaceSelect', () => {
+ it('initializes deprecatedJQueryDropdown', () => {
const dropdown = document.createElement('div');
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- expect($.fn.glDropdown).toHaveBeenCalled();
+ expect(initDeprecatedJQueryDropdown).toHaveBeenCalled();
});
describe('as input', () => {
- let glDropdownOptions;
+ let deprecatedJQueryDropdownOptions;
beforeEach(() => {
const dropdown = document.createElement('div');
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- [[glDropdownOptions]] = $.fn.glDropdown.mock.calls;
+ [[, deprecatedJQueryDropdownOptions]] = initDeprecatedJQueryDropdown.mock.calls;
});
it('prevents click events', () => {
const dummyEvent = new Event('dummy');
jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {});
- glDropdownOptions.clicked({ e: dummyEvent });
+ // expect(foo).toContain('test');
+ deprecatedJQueryDropdownOptions.clicked({ e: dummyEvent });
expect(dummyEvent.preventDefault).toHaveBeenCalled();
});
});
describe('as filter', () => {
- let glDropdownOptions;
+ let deprecatedJQueryDropdownOptions;
beforeEach(() => {
const dropdown = document.createElement('div');
dropdown.dataset.isFilter = 'true';
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- [[glDropdownOptions]] = $.fn.glDropdown.mock.calls;
+ [[, deprecatedJQueryDropdownOptions]] = initDeprecatedJQueryDropdown.mock.calls;
});
it('does not prevent click events', () => {
const dummyEvent = new Event('dummy');
jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {});
- glDropdownOptions.clicked({ e: dummyEvent });
+ deprecatedJQueryDropdownOptions.clicked({ e: dummyEvent });
expect(dummyEvent.preventDefault).not.toHaveBeenCalled();
});
@@ -58,7 +57,7 @@ describe('NamespaceSelect', () => {
it('sets URL of dropdown items', () => {
const dummyNamespace = { id: 'eal' };
- const itemUrl = glDropdownOptions.url(dummyNamespace);
+ const itemUrl = deprecatedJQueryDropdownOptions.url(dummyNamespace);
expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`);
});
diff --git a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
index b1a718d58b5..13af29821d8 100644
--- a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
+++ b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
@@ -12,7 +12,7 @@ exports[`JumpToNextDiscussionButton matches the snapshot 1`] = `
data-track-property="click_next_unresolved_thread"
title="Jump to next unresolved thread"
>
- <icon-stub
+ <gl-icon-stub
name="comment-next"
size="16"
/>
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index dc68c4371aa..59fa7b372ed 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -267,15 +267,14 @@ describe('issue_comment_form component', () => {
});
describe('when clicking close/reopen button', () => {
- it('should disable button and show a loading spinner', done => {
+ it('should disable button and show a loading spinner', () => {
const toggleStateButton = wrapper.find('.js-action-button');
toggleStateButton.trigger('click');
- wrapper.vm.$nextTick(() => {
- expect(toggleStateButton.element.disabled).toEqual(true);
- expect(toggleStateButton.find('.js-loading-button-icon').exists()).toBe(true);
- done();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(toggleStateButton.element.disabled).toEqual(true);
+ expect(toggleStateButton.props('loading')).toBe(true);
});
});
});
@@ -321,4 +320,33 @@ describe('issue_comment_form component', () => {
expect(wrapper.find('textarea').exists()).toBe(false);
});
});
+
+ describe('when issuable is open', () => {
+ beforeEach(() => {
+ setupStore(userDataMock, noteableDataMock);
+ });
+
+ it.each([['opened', 'warning'], ['reopened', 'warning']])(
+ 'when %i, it changes the variant of the btn to %i',
+ (a, expected) => {
+ store.state.noteableData.state = a;
+
+ mountComponent();
+
+ expect(wrapper.find('.js-action-button').props('variant')).toBe(expected);
+ },
+ );
+ });
+
+ describe('when issuable is not open', () => {
+ beforeEach(() => {
+ setupStore(userDataMock, noteableDataMock);
+
+ mountComponent();
+ });
+
+ it('should render the "default" variant of the button', () => {
+ expect(wrapper.find('.js-action-button').props('variant')).toBe('warning');
+ });
+ });
});
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index 04535aa17c5..affd6c1d1d2 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -1,5 +1,6 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import notesModule from '~/notes/stores/modules';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
@@ -112,13 +113,13 @@ describe('DiscussionCounter component', () => {
updateStoreWithExpanded(true);
expect(wrapper.vm.allExpanded).toBe(true);
- expect(toggleAllButton.find({ name: 'angle-up' }).exists()).toBe(true);
+ expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-up');
toggleAllButton.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.allExpanded).toBe(false);
- expect(toggleAllButton.find({ name: 'angle-down' }).exists()).toBe(true);
+ expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-down');
});
});
@@ -126,13 +127,13 @@ describe('DiscussionCounter component', () => {
updateStoreWithExpanded(false);
expect(wrapper.vm.allExpanded).toBe(false);
- expect(toggleAllButton.find({ name: 'angle-down' }).exists()).toBe(true);
+ expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-down');
toggleAllButton.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.allExpanded).toBe(true);
- expect(toggleAllButton.find({ name: 'angle-up' }).exists()).toBe(true);
+ expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-up');
});
});
});
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index 9a7896475e6..91ff796b9de 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -150,7 +150,7 @@ describe('DiscussionFilter component', () => {
eventHub.$emit('MergeRequestTabChange', 'commit');
wrapper.vm.$nextTick(() => {
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
done();
});
});
diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js
index c64e299efc3..41701e54dfa 100644
--- a/spec/frontend/notes/components/discussion_resolve_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import resolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
const buttonTitle = 'Resolve discussion';
@@ -26,9 +27,9 @@ describe('resolveDiscussionButton', () => {
});
it('should emit a onClick event on button click', () => {
- const button = wrapper.find({ ref: 'button' });
+ const button = wrapper.find(GlButton);
- button.trigger('click');
+ button.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted()).toEqual({
@@ -38,7 +39,7 @@ describe('resolveDiscussionButton', () => {
});
it('should contain the provided button title', () => {
- const button = wrapper.find({ ref: 'button' });
+ const button = wrapper.find(GlButton);
expect(button.text()).toContain(buttonTitle);
});
@@ -51,9 +52,9 @@ describe('resolveDiscussionButton', () => {
},
});
- const button = wrapper.find({ ref: 'isResolvingIcon' });
+ const button = wrapper.find(GlButton);
- expect(button.exists()).toEqual(true);
+ expect(button.props('loading')).toEqual(true);
});
it('should only show a loading spinner while resolving', () => {
@@ -64,10 +65,10 @@ describe('resolveDiscussionButton', () => {
},
});
- const button = wrapper.find({ ref: 'isResolvingIcon' });
+ const button = wrapper.find(GlButton);
wrapper.vm.$nextTick(() => {
- expect(button.exists()).toEqual(false);
+ expect(button.props('loading')).toEqual(false);
});
});
});
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 97d1752726b..a79c3bbacb7 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -35,8 +35,12 @@ describe('noteActions', () => {
canEdit: true,
canAwardEmoji: true,
canReportAsAbuse: true,
+ isAuthor: true,
+ isContributor: false,
+ noteableType: 'MergeRequest',
noteId: '539',
noteUrl: `${TEST_HOST}/group/project/-/merge_requests/1#note_1`,
+ projectName: 'project',
reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`,
showReply: false,
};
@@ -60,15 +64,43 @@ describe('noteActions', () => {
wrapper = shallowMountNoteActions(props);
});
+ it('should render noteable author badge', () => {
+ expect(
+ wrapper
+ .findAll('.note-role')
+ .at(0)
+ .text()
+ .trim(),
+ ).toEqual('Author');
+ });
+
it('should render access level badge', () => {
expect(
wrapper
- .find('.note-role')
+ .findAll('.note-role')
+ .at(1)
.text()
.trim(),
).toEqual(props.accessLevel);
});
+ it('should render contributor badge', () => {
+ wrapper.setProps({
+ accessLevel: null,
+ isContributor: true,
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(
+ wrapper
+ .findAll('.note-role')
+ .at(1)
+ .text()
+ .trim(),
+ ).toBe('Contributor');
+ });
+ });
+
it('should render emoji link', () => {
expect(wrapper.find('.js-add-award').exists()).toBe(true);
expect(wrapper.find('.js-add-award').attributes('data-position')).toBe('right');
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index fbfba2efb1d..c6034639a4a 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -330,6 +330,8 @@ describe('note_app', () => {
wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent);
+ jest.advanceTimersByTime(2);
+
expect(toggleAwardAction).toHaveBeenCalledTimes(1);
const [, payload] = toggleAwardAction.mock.calls[0];
diff --git a/spec/frontend/notes/helpers.js b/spec/frontend/notes/helpers.js
index 3f349b40ba5..c8168a49a5b 100644
--- a/spec/frontend/notes/helpers.js
+++ b/spec/frontend/notes/helpers.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
store.replaceState({
notes: [],
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 11c0bbfefc9..d203435e7bf 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -194,13 +194,9 @@ describe('Discussion navigation mixin', () => {
});
it('expands discussion', () => {
- expect(expandDiscussion).toHaveBeenCalledWith(
- expect.anything(),
- {
- discussionId: expected,
- },
- undefined,
- );
+ expect(expandDiscussion).toHaveBeenCalledWith(expect.anything(), {
+ discussionId: expected,
+ });
});
it('scrolls to discussion', () => {
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 6b8d0790669..4681f3aa429 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -3,6 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import Api from '~/api';
import { deprecatedCreateFlash as Flash } from '~/flash';
import * as actions from '~/notes/stores/actions';
+import mutations from '~/notes/stores/mutations';
import * as mutationTypes from '~/notes/stores/mutation_types';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
@@ -334,6 +335,9 @@ describe('Actions Notes Store', () => {
it('calls service with last fetched state', done => {
store
.dispatch('poll')
+ .then(() => {
+ jest.advanceTimersByTime(2);
+ })
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
.then(() => {
expect(store.state.lastFetchedAt).toBe('123456');
@@ -651,6 +655,26 @@ describe('Actions Notes Store', () => {
});
describe('updateOrCreateNotes', () => {
+ it('Prevents `fetchDiscussions` being called multiple times within time limit', () => {
+ jest.useFakeTimers();
+ const note = { id: 1234, type: notesConstants.DIFF_NOTE };
+ const getters = { notesById: {} };
+ state = { discussions: [note], notesData: { discussionsPath: '' } };
+ commit.mockImplementation((type, value) => {
+ if (type === mutationTypes.SET_FETCHING_DISCUSSIONS) {
+ mutations[type](state, value);
+ }
+ });
+
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]);
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]);
+
+ jest.runAllTimers();
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]);
+
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ });
+
it('Updates existing note', () => {
const note = { id: 1234 };
const getters = { notesById: { 1234: note } };
diff --git a/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap
deleted file mode 100644
index 172b8919673..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap
+++ /dev/null
@@ -1,46 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Package code instruction multiline to match the snapshot 1`] = `
-<div>
- <pre
- class="js-instruction-pre"
- >
- this is some
-multiline text
- </pre>
-</div>
-`;
-
-exports[`Package code instruction single line to match the default snapshot 1`] = `
-<div
- class="input-group gl-mb-3"
->
- <input
- class="form-control monospace js-instruction-input"
- readonly="readonly"
- type="text"
- />
-
- <span
- class="input-group-append js-instruction-button"
- >
- <button
- class="btn input-group-text btn-secondary btn-md btn-default"
- data-clipboard-text="npm i @my-package"
- title="Copy npm install command"
- type="button"
- >
- <!---->
-
- <svg
- class="gl-icon s16"
- data-testid="copy-to-clipboard-icon"
- >
- <use
- href="#copy-to-clipboard"
- />
- </svg>
- </button>
- </span>
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
index 852292e084b..a1d08f032bc 100644
--- a/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
@@ -8,18 +8,12 @@ exports[`ConanInstallation renders all the messages 1`] = `
Installation
</h3>
- <h4
- class="gl-font-base"
- >
-
- Conan Command
-
- </h4>
-
<code-instruction-stub
copytext="Copy Conan Command"
instruction="foo/command"
+ label="Conan Command"
trackingaction="copy_conan_command"
+ trackinglabel="code_instruction"
/>
<h3
@@ -28,18 +22,12 @@ exports[`ConanInstallation renders all the messages 1`] = `
Registry setup
</h3>
- <h4
- class="gl-font-base"
- >
-
- Add Conan Remote
-
- </h4>
-
<code-instruction-stub
copytext="Copy Conan Setup Command"
instruction="foo/setup"
+ label="Add Conan Remote"
trackingaction="copy_conan_setup_command"
+ trackinglabel="code_instruction"
/>
<gl-sprintf-stub
diff --git a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
index 28b7ca442eb..39469bf4fd0 100644
--- a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
@@ -21,7 +21,7 @@ exports[`DependencyRow renders full dependency 1`] = `
</div>
<div
- class="table-section section-50 gl-display-flex justify-content-md-end"
+ class="table-section section-50 gl-display-flex gl-md-justify-content-end"
data-testid="version-pattern"
>
<span
diff --git a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
index 10e54500797..6d22b372d41 100644
--- a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
@@ -8,14 +8,6 @@ exports[`MavenInstallation renders all the messages 1`] = `
Installation
</h3>
- <h4
- class="gl-font-base"
- >
-
- Maven XML
-
- </h4>
-
<p>
<gl-sprintf-stub
message="Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
@@ -25,22 +17,18 @@ exports[`MavenInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy Maven XML"
instruction="foo/xml"
+ label="Maven XML"
multiline="true"
trackingaction="copy_maven_xml"
+ trackinglabel="code_instruction"
/>
- <h4
- class="gl-font-base"
- >
-
- Maven Command
-
- </h4>
-
<code-instruction-stub
copytext="Copy Maven command"
instruction="foo/command"
+ label="Maven Command"
trackingaction="copy_maven_command"
+ trackinglabel="code_instruction"
/>
<h3
@@ -58,8 +46,10 @@ exports[`MavenInstallation renders all the messages 1`] = `
<code-instruction-stub
copytext="Copy Maven registry XML"
instruction="foo/setup"
+ label=""
multiline="true"
trackingaction="copy_maven_setup_xml"
+ trackinglabel="code_instruction"
/>
<gl-sprintf-stub
diff --git a/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
index 58a509e6847..b616751f75f 100644
--- a/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
@@ -8,28 +8,20 @@ exports[`NpmInstallation renders all the messages 1`] = `
Installation
</h3>
- <h4
- class="gl-font-base"
- >
- npm command
- </h4>
-
<code-instruction-stub
copytext="Copy npm command"
instruction="npm i @Test/package"
+ label="npm command"
trackingaction="copy_npm_install_command"
+ trackinglabel="code_instruction"
/>
- <h4
- class="gl-font-base"
- >
- yarn command
- </h4>
-
<code-instruction-stub
copytext="Copy yarn command"
instruction="yarn add @Test/package"
+ label="yarn command"
trackingaction="copy_yarn_install_command"
+ trackinglabel="code_instruction"
/>
<h3
@@ -38,28 +30,20 @@ exports[`NpmInstallation renders all the messages 1`] = `
Registry setup
</h3>
- <h4
- class="gl-font-base"
- >
- npm command
- </h4>
-
<code-instruction-stub
copytext="Copy npm setup command"
- instruction="echo @Test:registry=undefined >> .npmrc"
+ instruction="echo @Test:registry=undefined/ >> .npmrc"
+ label="npm command"
trackingaction="copy_npm_setup_command"
+ trackinglabel="code_instruction"
/>
- <h4
- class="gl-font-base"
- >
- yarn command
- </h4>
-
<code-instruction-stub
copytext="Copy yarn setup command"
- instruction="echo \\\\\\"@Test:registry\\\\\\" \\\\\\"undefined\\\\\\" >> .yarnrc"
+ instruction="echo \\\\\\"@Test:registry\\\\\\" \\\\\\"undefined/\\\\\\" >> .yarnrc"
+ label="yarn command"
trackingaction="copy_yarn_setup_command"
+ trackinglabel="code_instruction"
/>
<gl-sprintf-stub
diff --git a/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
index 67810290c62..84cf5e4db49 100644
--- a/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
@@ -8,18 +8,12 @@ exports[`NugetInstallation renders all the messages 1`] = `
Installation
</h3>
- <h4
- class="gl-font-base"
- >
-
- NuGet Command
-
- </h4>
-
<code-instruction-stub
copytext="Copy NuGet Command"
instruction="foo/command"
+ label="NuGet Command"
trackingaction="copy_nuget_install_command"
+ trackinglabel="code_instruction"
/>
<h3
@@ -28,18 +22,12 @@ exports[`NugetInstallation renders all the messages 1`] = `
Registry setup
</h3>
- <h4
- class="gl-font-base"
- >
-
- Add NuGet Source
-
- </h4>
-
<code-instruction-stub
copytext="Copy NuGet Setup Command"
instruction="foo/setup"
+ label="Add NuGet Source"
trackingaction="copy_nuget_setup_command"
+ trackinglabel="code_instruction"
/>
<gl-sprintf-stub
diff --git a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
index bdcd4a9e077..4d9e0af1545 100644
--- a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
@@ -2,171 +2,151 @@
exports[`PackageTitle renders with tags 1`] = `
<div
- class="gl-flex-direction-column"
+ class="gl-display-flex gl-justify-content-space-between gl-py-3"
+ data-qa-selector="package_title"
>
<div
- class="gl-display-flex"
+ class="gl-flex-direction-column"
>
- <!---->
-
<div
- class="gl-display-flex gl-flex-direction-column"
+ class="gl-display-flex"
>
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
- >
-
- Test package
-
- </h1>
+ <!---->
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500"
+ class="gl-display-flex gl-flex-direction-column"
>
- <gl-icon-stub
- class="gl-mr-3"
- name="eye"
- size="16"
- />
+ <h1
+ class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ data-testid="title"
+ >
+ Test package
+ </h1>
- <gl-sprintf-stub
- message="v%{version} published %{timeAgo}"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ >
+ <gl-icon-stub
+ class="gl-mr-3"
+ name="eye"
+ size="16"
+ />
+
+ <gl-sprintf-stub
+ message="v%{version} published %{timeAgo}"
+ />
+ </div>
</div>
</div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <gl-icon-stub
- class="gl-text-gray-500 gl-mr-3"
- name="package"
- size="16"
- />
-
- <span
- class="gl-font-weight-bold"
- data-testid="package-type"
- >
- maven
- </span>
- </div>
<div
- class="gl-display-flex gl-align-items-center gl-mr-5"
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
- <package-tags-stub
- tagdisplaylimit="1"
- tags="[object Object],[object Object],[object Object],[object Object]"
- />
- </div>
-
- <!---->
-
- <!---->
-
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <gl-icon-stub
- class="gl-text-gray-500 gl-mr-3"
- name="disk"
- size="16"
- />
-
- <span
- class="gl-font-weight-bold"
- data-testid="package-size"
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <metadata-item-stub
+ data-testid="package-type"
+ icon="package"
+ link=""
+ size="s"
+ text="maven"
+ />
+ </div>
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <metadata-item-stub
+ data-testid="package-size"
+ icon="disk"
+ link=""
+ size="s"
+ text="300 bytes"
+ />
+ </div>
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
>
- 300 bytes
- </span>
+ <package-tags-stub
+ hidelabel="true"
+ tagdisplaylimit="2"
+ tags="[object Object],[object Object],[object Object],[object Object]"
+ />
+ </div>
</div>
</div>
+
+ <!---->
</div>
`;
exports[`PackageTitle renders without tags 1`] = `
<div
- class="gl-flex-direction-column"
+ class="gl-display-flex gl-justify-content-space-between gl-py-3"
+ data-qa-selector="package_title"
>
<div
- class="gl-display-flex"
+ class="gl-flex-direction-column"
>
- <!---->
-
<div
- class="gl-display-flex gl-flex-direction-column"
+ class="gl-display-flex"
>
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
- >
-
- Test package
-
- </h1>
+ <!---->
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500"
+ class="gl-display-flex gl-flex-direction-column"
>
- <gl-icon-stub
- class="gl-mr-3"
- name="eye"
- size="16"
- />
+ <h1
+ class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ data-testid="title"
+ >
+ Test package
+ </h1>
- <gl-sprintf-stub
- message="v%{version} published %{timeAgo}"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ >
+ <gl-icon-stub
+ class="gl-mr-3"
+ name="eye"
+ size="16"
+ />
+
+ <gl-sprintf-stub
+ message="v%{version} published %{timeAgo}"
+ />
+ </div>
</div>
</div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <gl-icon-stub
- class="gl-text-gray-500 gl-mr-3"
- name="package"
- size="16"
- />
-
- <span
- class="gl-font-weight-bold"
- data-testid="package-type"
- >
- maven
- </span>
- </div>
-
- <!---->
-
- <!---->
-
- <!---->
<div
- class="gl-display-flex gl-align-items-center gl-mr-5"
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
- <gl-icon-stub
- class="gl-text-gray-500 gl-mr-3"
- name="disk"
- size="16"
- />
-
- <span
- class="gl-font-weight-bold"
- data-testid="package-size"
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
>
- 300 bytes
- </span>
+ <metadata-item-stub
+ data-testid="package-type"
+ icon="package"
+ link=""
+ size="s"
+ text="maven"
+ />
+ </div>
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <metadata-item-stub
+ data-testid="package-size"
+ icon="disk"
+ link=""
+ size="s"
+ text="300 bytes"
+ />
+ </div>
</div>
</div>
+
+ <!---->
</div>
`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
index 5c1e74d73af..2a588f99c1d 100644
--- a/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
@@ -8,19 +8,13 @@ exports[`PypiInstallation renders all the messages 1`] = `
Installation
</h3>
- <h4
- class="gl-font-base"
- >
-
- Pip Command
-
- </h4>
-
<code-instruction-stub
copytext="Copy Pip command"
data-testid="pip-command"
instruction="pip install"
+ label="Pip Command"
trackingaction="copy_pip_install_command"
+ trackinglabel="code_instruction"
/>
<h3
@@ -39,8 +33,10 @@ exports[`PypiInstallation renders all the messages 1`] = `
copytext="Copy .pypirc content"
data-testid="pypi-setup-content"
instruction="python setup"
+ label=""
multiline="true"
trackingaction="copy_pypi_setup_command"
+ trackinglabel="code_instruction"
/>
<gl-sprintf-stub
diff --git a/spec/frontend/packages/details/components/additional_metadata_spec.js b/spec/frontend/packages/details/components/additional_metadata_spec.js
index b2337b86740..111e4205abb 100644
--- a/spec/frontend/packages/details/components/additional_metadata_spec.js
+++ b/spec/frontend/packages/details/components/additional_metadata_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
-import DetailsRow from '~/registry/shared/components/details_row.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import component from '~/packages/details/components/additional_metadata.vue';
import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';
diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js
index f535f3f5744..e82c74e56e5 100644
--- a/spec/frontend/packages/details/components/app_spec.js
+++ b/spec/frontend/packages/details/components/app_spec.js
@@ -34,12 +34,15 @@ describe('PackagesApp', () => {
let wrapper;
let store;
const fetchPackageVersions = jest.fn();
+ const deletePackage = jest.fn();
+ const defaultProjectName = 'bar';
+ const { location } = window;
function createComponent({
packageEntity = mavenPackage,
packageFiles = mavenFiles,
isLoading = false,
- oneColumnView = false,
+ projectName = defaultProjectName,
} = {}) {
store = new Vuex.Store({
state: {
@@ -47,14 +50,15 @@ describe('PackagesApp', () => {
packageEntity,
packageFiles,
canDelete: true,
- destroyPath: 'destroy-package-path',
emptySvgPath: 'empty-illustration',
npmPath: 'foo',
npmHelpPath: 'foo',
- projectName: 'bar',
- oneColumnView,
+ projectName,
+ projectListUrl: 'project_url',
+ groupListUrl: 'group_url',
},
actions: {
+ deletePackage,
fetchPackageVersions,
},
getters,
@@ -65,6 +69,8 @@ describe('PackagesApp', () => {
store,
stubs: {
...stubChildren(PackagesApp),
+ PackageTitle: false,
+ TitleArea: false,
GlButton: false,
GlModal: false,
GlTab: false,
@@ -93,8 +99,14 @@ describe('PackagesApp', () => {
const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
const findInstallationCommands = () => wrapper.find(InstallationCommands);
+ beforeEach(() => {
+ delete window.location;
+ window.location = { replace: jest.fn() };
+ });
+
afterEach(() => {
wrapper.destroy();
+ window.location = location;
});
it('renders the app and displays the package title', () => {
@@ -238,44 +250,94 @@ describe('PackagesApp', () => {
});
});
- describe('tracking', () => {
- let eventSpy;
- let utilSpy;
- const category = 'foo';
+ describe('tracking and delete', () => {
+ const doDelete = async () => {
+ deleteButton().trigger('click');
+ await wrapper.vm.$nextTick();
+ modalDeleteButton().trigger('click');
+ };
+
+ describe('delete', () => {
+ const originalReferrer = document.referrer;
+ const setReferrer = (value = defaultProjectName) => {
+ Object.defineProperty(document, 'referrer', {
+ value,
+ configurable: true,
+ });
+ };
+
+ afterEach(() => {
+ Object.defineProperty(document, 'referrer', {
+ value: originalReferrer,
+ configurable: true,
+ });
+ });
- beforeEach(() => {
- eventSpy = jest.spyOn(Tracking, 'event');
- utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
- });
+ it('calls the proper vuex action', async () => {
+ createComponent({ packageEntity: npmPackage });
+ await doDelete();
+ expect(deletePackage).toHaveBeenCalled();
+ });
- it('tracking category calls packageTypeToTrackCategory', () => {
- createComponent({ packageEntity: conanPackage });
- expect(wrapper.vm.tracking.category).toBe(category);
- expect(utilSpy).toHaveBeenCalledWith('conan');
+ it('when referrer contains project name calls window.replace with project url', async () => {
+ setReferrer();
+ deletePackage.mockResolvedValue();
+ createComponent({ packageEntity: npmPackage });
+ await doDelete();
+ await deletePackage();
+ expect(window.location.replace).toHaveBeenCalledWith(
+ 'project_url?showSuccessDeleteAlert=true',
+ );
+ });
+
+ it('when referrer does not contain project name calls window.replace with group url', async () => {
+ setReferrer('baz');
+ deletePackage.mockResolvedValue();
+ createComponent({ packageEntity: npmPackage });
+ await doDelete();
+ await deletePackage();
+ expect(window.location.replace).toHaveBeenCalledWith(
+ 'group_url?showSuccessDeleteAlert=true',
+ );
+ });
});
- it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => {
- createComponent({ packageEntity: conanPackage });
- deleteButton().trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- modalDeleteButton().trigger('click');
+ describe('tracking', () => {
+ let eventSpy;
+ let utilSpy;
+ const category = 'foo';
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
+ });
+
+ it('tracking category calls packageTypeToTrackCategory', () => {
+ createComponent({ packageEntity: conanPackage });
+ expect(wrapper.vm.tracking.category).toBe(category);
+ expect(utilSpy).toHaveBeenCalledWith('conan');
+ });
+
+ it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, async () => {
+ createComponent({ packageEntity: npmPackage });
+ await doDelete();
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.DELETE_PACKAGE,
expect.any(Object),
);
});
- });
- it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
- createComponent({ packageEntity: conanPackage });
+ it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
+ createComponent({ packageEntity: conanPackage });
- firstFileDownloadLink().vm.$emit('click');
- expect(eventSpy).toHaveBeenCalledWith(
- category,
- TrackingActions.PULL_PACKAGE,
- expect.any(Object),
- );
+ firstFileDownloadLink().vm.$emit('click');
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.PULL_PACKAGE,
+ expect.any(Object),
+ );
+ });
});
});
});
diff --git a/spec/frontend/packages/details/components/composer_installation_spec.js b/spec/frontend/packages/details/components/composer_installation_spec.js
index 7679d721391..c13981fbb87 100644
--- a/spec/frontend/packages/details/components/composer_installation_spec.js
+++ b/spec/frontend/packages/details/components/composer_installation_spec.js
@@ -4,7 +4,7 @@ import { GlSprintf, GlLink } from '@gitlab/ui';
import { registryUrl as composerHelpPath } from 'jest/packages/details/mock_data';
import { composerPackage as packageEntity } from 'jest/packages/mock_data';
import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
-import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+
import { TrackingActions } from '~/packages/details/constants';
const localVue = createLocalVue();
@@ -27,9 +27,8 @@ describe('ComposerInstallation', () => {
},
});
- const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
- const findRegistryIncludeTitle = () => wrapper.find('[data-testid="registry-include-title"]');
- const findPackageIncludeTitle = () => wrapper.find('[data-testid="package-include-title"]');
+ const findRegistryInclude = () => wrapper.find('[data-testid="registry-include"]');
+ const findPackageInclude = () => wrapper.find('[data-testid="package-include"]');
const findHelpText = () => wrapper.find('[data-testid="help-text"]');
const findHelpLink = () => wrapper.find(GlLink);
@@ -53,7 +52,7 @@ describe('ComposerInstallation', () => {
describe('registry include command', () => {
it('uses code_instructions', () => {
- const registryIncludeCommand = findCodeInstructions().at(0);
+ const registryIncludeCommand = findRegistryInclude();
expect(registryIncludeCommand.exists()).toBe(true);
expect(registryIncludeCommand.props()).toMatchObject({
instruction: composerRegistryIncludeStr,
@@ -63,13 +62,13 @@ describe('ComposerInstallation', () => {
});
it('has the correct title', () => {
- expect(findRegistryIncludeTitle().text()).toBe('composer.json registry include');
+ expect(findRegistryInclude().props('label')).toBe('composer.json registry include');
});
});
describe('package include command', () => {
it('uses code_instructions', () => {
- const registryIncludeCommand = findCodeInstructions().at(1);
+ const registryIncludeCommand = findPackageInclude();
expect(registryIncludeCommand.exists()).toBe(true);
expect(registryIncludeCommand.props()).toMatchObject({
instruction: composerPackageIncludeStr,
@@ -79,7 +78,7 @@ describe('ComposerInstallation', () => {
});
it('has the correct title', () => {
- expect(findPackageIncludeTitle().text()).toBe('composer.json require package include');
+ expect(findPackageInclude().props('label')).toBe('composer.json require package include');
});
it('has the correct help text', () => {
diff --git a/spec/frontend/packages/details/components/conan_installation_spec.js b/spec/frontend/packages/details/components/conan_installation_spec.js
index 5b31e38dad5..c79d1bb50dd 100644
--- a/spec/frontend/packages/details/components/conan_installation_spec.js
+++ b/spec/frontend/packages/details/components/conan_installation_spec.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import ConanInstallation from '~/packages/details/components/conan_installation.vue';
-import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { conanPackage as packageEntity } from '../../mock_data';
import { registryUrl as conanPath } from '../mock_data';
diff --git a/spec/frontend/packages/details/components/maven_installation_spec.js b/spec/frontend/packages/details/components/maven_installation_spec.js
index 5d0007294b6..f301a03a7f3 100644
--- a/spec/frontend/packages/details/components/maven_installation_spec.js
+++ b/spec/frontend/packages/details/components/maven_installation_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { registryUrl as mavenPath } from 'jest/packages/details/mock_data';
import { mavenPackage as packageEntity } from 'jest/packages/mock_data';
import MavenInstallation from '~/packages/details/components/maven_installation.vue';
-import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { TrackingActions } from '~/packages/details/constants';
const localVue = createLocalVue();
diff --git a/spec/frontend/packages/details/components/npm_installation_spec.js b/spec/frontend/packages/details/components/npm_installation_spec.js
index f47bac57a66..4223a05453c 100644
--- a/spec/frontend/packages/details/components/npm_installation_spec.js
+++ b/spec/frontend/packages/details/components/npm_installation_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { npmPackage as packageEntity } from 'jest/packages/mock_data';
import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
import NpmInstallation from '~/packages/details/components/npm_installation.vue';
-import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { TrackingActions } from '~/packages/details/constants';
import { npmInstallationCommand, npmSetupCommand } from '~/packages/details/store/getters';
@@ -78,7 +78,7 @@ describe('NpmInstallation', () => {
.at(2)
.props(),
).toMatchObject({
- instruction: 'echo @Test:registry=undefined >> .npmrc',
+ instruction: 'echo @Test:registry=undefined/ >> .npmrc',
multiline: false,
trackingAction: TrackingActions.COPY_NPM_SETUP_COMMAND,
});
@@ -90,7 +90,7 @@ describe('NpmInstallation', () => {
.at(3)
.props(),
).toMatchObject({
- instruction: 'echo \\"@Test:registry\\" \\"undefined\\" >> .yarnrc',
+ instruction: 'echo \\"@Test:registry\\" \\"undefined/\\" >> .yarnrc',
multiline: false,
trackingAction: TrackingActions.COPY_YARN_SETUP_COMMAND,
});
diff --git a/spec/frontend/packages/details/components/nuget_installation_spec.js b/spec/frontend/packages/details/components/nuget_installation_spec.js
index a23bf9a18a1..b381d131e94 100644
--- a/spec/frontend/packages/details/components/nuget_installation_spec.js
+++ b/spec/frontend/packages/details/components/nuget_installation_spec.js
@@ -3,7 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nugetPackage as packageEntity } from 'jest/packages/mock_data';
import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
-import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { TrackingActions } from '~/packages/details/constants';
const localVue = createLocalVue();
diff --git a/spec/frontend/packages/details/components/package_history_spec.js b/spec/frontend/packages/details/components/package_history_spec.js
index e293e119585..f745a457b0a 100644
--- a/spec/frontend/packages/details/components/package_history_spec.js
+++ b/spec/frontend/packages/details/components/package_history_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import component from '~/packages/details/components/package_history.vue';
import { mavenPackage, mockPipelineInfo } from '../../mock_data';
@@ -16,7 +17,10 @@ describe('Package History', () => {
wrapper = shallowMount(component, {
propsData: { ...defaultProps, ...props },
stubs: {
- HistoryElement: '<div data-testid="history-element"><slot></slot></div>',
+ HistoryItem: {
+ props: HistoryItem.props,
+ template: '<div data-testid="history-element"><slot></slot></div>',
+ },
GlSprintf,
},
});
diff --git a/spec/frontend/packages/details/components/package_title_spec.js b/spec/frontend/packages/details/components/package_title_spec.js
index a30dc4b8aba..d0ed78418af 100644
--- a/spec/frontend/packages/details/components/package_title_spec.js
+++ b/spec/frontend/packages/details/components/package_title_spec.js
@@ -2,6 +2,7 @@ import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import PackageTitle from '~/packages/details/components/package_title.vue';
import PackageTags from '~/packages/shared/components/package_tags.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
conanPackage,
mavenFiles,
@@ -39,10 +40,14 @@ describe('PackageTitle', () => {
wrapper = shallowMount(PackageTitle, {
localVue,
store,
+ stubs: {
+ TitleArea,
+ },
});
+ return wrapper.vm.$nextTick();
}
- const packageIcon = () => wrapper.find('[data-testid="package-icon"]');
+ const findTitleArea = () => wrapper.find(TitleArea);
const packageType = () => wrapper.find('[data-testid="package-type"]');
const packageSize = () => wrapper.find('[data-testid="package-size"]');
const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
@@ -54,72 +59,74 @@ describe('PackageTitle', () => {
});
describe('renders', () => {
- it('without tags', () => {
- createComponent();
+ it('without tags', async () => {
+ await createComponent();
expect(wrapper.element).toMatchSnapshot();
});
- it('with tags', () => {
- createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
+ it('with tags', async () => {
+ await createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
expect(wrapper.element).toMatchSnapshot();
});
});
- describe('package icon', () => {
- const fakeSrc = 'a-fake-src';
-
- it('shows an icon when provided one from vuex', () => {
- createComponent({ icon: fakeSrc });
+ describe('package title', () => {
+ it('is correctly bound', async () => {
+ await createComponent();
- expect(packageIcon().exists()).toBe(true);
+ expect(findTitleArea().props('title')).toBe('Test package');
});
+ });
- it('has the correct src attribute', () => {
- createComponent({ icon: fakeSrc });
+ describe('package icon', () => {
+ const fakeSrc = 'a-fake-src';
+
+ it('binds an icon when provided one from vuex', async () => {
+ await createComponent({ icon: fakeSrc });
- expect(packageIcon().props('src')).toBe(fakeSrc);
+ expect(findTitleArea().props('avatar')).toBe(fakeSrc);
});
- it('does not show an icon when not provided one', () => {
- createComponent();
+ it('do not binds an icon when not provided one', async () => {
+ await createComponent();
- expect(packageIcon().exists()).toBe(false);
+ expect(findTitleArea().props('avatar')).toBe(null);
});
});
describe.each`
- packageEntity | expectedResult
+ packageEntity | text
${conanPackage} | ${'conan'}
${mavenPackage} | ${'maven'}
${npmPackage} | ${'npm'}
${nugetPackage} | ${'nuget'}
- `(`package type`, ({ packageEntity, expectedResult }) => {
+ `(`package type`, ({ packageEntity, text }) => {
beforeEach(() => createComponent({ packageEntity }));
- it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => {
- expect(packageType().text()).toBe(expectedResult);
+ it(`${packageEntity.package_type} should render from Vuex getters ${text}`, () => {
+ expect(packageType().props()).toEqual(expect.objectContaining({ text, icon: 'package' }));
});
});
describe('calculates the package size', () => {
- it('correctly calulates when there is only 1 file', () => {
- createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
+ it('correctly calculates when there is only 1 file', async () => {
+ await createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
- expect(packageSize().text()).toBe('200 bytes');
+ expect(packageSize().props()).toMatchObject({ text: '200 bytes', icon: 'disk' });
});
- it('correctly calulates when there are multiple files', () => {
- createComponent();
+ it('correctly calulates when there are multiple files', async () => {
+ await createComponent();
- expect(packageSize().text()).toBe('300 bytes');
+ expect(packageSize().props('text')).toBe('300 bytes');
});
});
describe('package tags', () => {
- it('displays the package-tags component when the package has tags', () => {
- createComponent({
+ it('displays the package-tags component when the package has tags', async () => {
+ await createComponent({
packageEntity: {
...npmPackage,
tags: mockTags,
@@ -129,40 +136,44 @@ describe('PackageTitle', () => {
expect(packageTags().exists()).toBe(true);
});
- it('does not display the package-tags component when there are no tags', () => {
- createComponent();
+ it('does not display the package-tags component when there are no tags', async () => {
+ await createComponent();
expect(packageTags().exists()).toBe(false);
});
});
describe('package ref', () => {
- it('does not display the ref if missing', () => {
- createComponent();
+ it('does not display the ref if missing', async () => {
+ await createComponent();
expect(packageRef().exists()).toBe(false);
});
- it('correctly shows the package ref if there is one', () => {
- createComponent({ packageEntity: npmPackage });
-
- expect(packageRef().contains('gl-icon-stub')).toBe(true);
- expect(packageRef().text()).toBe(npmPackage.pipeline.ref);
+ it('correctly shows the package ref if there is one', async () => {
+ await createComponent({ packageEntity: npmPackage });
+ expect(packageRef().props()).toMatchObject({
+ text: npmPackage.pipeline.ref,
+ icon: 'branch',
+ });
});
});
describe('pipeline project', () => {
- it('does not display the project if missing', () => {
- createComponent();
+ it('does not display the project if missing', async () => {
+ await createComponent();
expect(pipelineProject().exists()).toBe(false);
});
- it('correctly shows the pipeline project if there is one', () => {
- createComponent({ packageEntity: npmPackage });
+ it('correctly shows the pipeline project if there is one', async () => {
+ await createComponent({ packageEntity: npmPackage });
- expect(pipelineProject().text()).toBe(npmPackage.pipeline.project.name);
- expect(pipelineProject().attributes('href')).toBe(npmPackage.pipeline.project.web_url);
+ expect(pipelineProject().props()).toMatchObject({
+ text: npmPackage.pipeline.project.name,
+ icon: 'review-list',
+ link: npmPackage.pipeline.project.web_url,
+ });
});
});
});
diff --git a/spec/frontend/packages/details/store/actions_spec.js b/spec/frontend/packages/details/store/actions_spec.js
index 6dfb2b63f85..70f87d18bcb 100644
--- a/spec/frontend/packages/details/store/actions_spec.js
+++ b/spec/frontend/packages/details/store/actions_spec.js
@@ -1,9 +1,10 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import fetchPackageVersions from '~/packages/details/store/actions';
+import { fetchPackageVersions, deletePackage } from '~/packages/details/store/actions';
import * as types from '~/packages/details/store/mutation_types';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
+import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import { npmPackage as packageEntity } from '../../mock_data';
jest.mock('~/flash.js');
@@ -73,4 +74,25 @@ describe('Actions Package details store', () => {
);
});
});
+
+ describe('deletePackage', () => {
+ it('should call Api.deleteProjectPackage', done => {
+ Api.deleteProjectPackage = jest.fn().mockResolvedValue();
+ testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
+ expect(Api.deleteProjectPackage).toHaveBeenCalledWith(
+ packageEntity.project_id,
+ packageEntity.id,
+ );
+ done();
+ });
+ });
+ it('should create flash on API error', done => {
+ Api.deleteProjectPackage = jest.fn().mockRejectedValue();
+
+ testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
+ expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE);
+ done();
+ });
+ });
+ });
});
diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js
index 307976d4124..0e95ee4cfd3 100644
--- a/spec/frontend/packages/details/store/getters_spec.js
+++ b/spec/frontend/packages/details/store/getters_spec.js
@@ -62,9 +62,9 @@ describe('Getters PackageDetails Store', () => {
const mavenSetupXmlBlock = generateMavenSetupXml();
const npmInstallStr = `npm i ${npmPackage.name}`;
- const npmSetupStr = `echo @Test:registry=${registryUrl} >> .npmrc`;
+ const npmSetupStr = `echo @Test:registry=${registryUrl}/ >> .npmrc`;
const yarnInstallStr = `yarn add ${npmPackage.name}`;
- const yarnSetupStr = `echo \\"@Test:registry\\" \\"${registryUrl}\\" >> .yarnrc`;
+ const yarnSetupStr = `echo \\"@Test:registry\\" \\"${registryUrl}/\\" >> .yarnrc`;
const nugetInstallationCommandStr = `nuget install ${nugetPackage.name} -Source "GitLab"`;
const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`;
diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
index 2b7a4c83bed..6ff9376565a 100644
--- a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -444,7 +444,7 @@ exports[`packages_list_app renders 1`] = `
</template>
<template>
<div
- class="d-flex align-self-center ml-md-auto py-1 py-md-0"
+ class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
>
<package-filter-stub
class="mr-1"
diff --git a/spec/frontend/packages/list/components/packages_list_app_spec.js b/spec/frontend/packages/list/components/packages_list_app_spec.js
index 31bab3886c1..19ff4290f50 100644
--- a/spec/frontend/packages/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_app_spec.js
@@ -1,7 +1,14 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState, GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
+import * as commonUtils from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
+
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -145,4 +152,46 @@ describe('packages_list_app', () => {
);
});
});
+
+ describe('delete alert handling', () => {
+ const { location } = window.location;
+ const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
+
+ beforeEach(() => {
+ createStore('foo');
+ jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
+ delete window.location;
+ window.location = {
+ href: `foo_bar_baz${search}`,
+ search,
+ };
+ });
+
+ afterEach(() => {
+ window.location = location;
+ });
+
+ it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
+ mountComponent();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_SUCCESS_MESSAGE,
+ type: 'notice',
+ });
+ });
+
+ it('calls historyReplaceState with a clean url', () => {
+ mountComponent();
+
+ expect(commonUtils.historyReplaceState).toHaveBeenCalledWith('foo_bar_baz');
+ });
+
+ it(`does nothing if the query string does not contain ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
+ window.location.search = '';
+ mountComponent();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/packages/list/components/packages_list_spec.js b/spec/frontend/packages/list/components/packages_list_spec.js
index a90d5056212..f981cc2851a 100644
--- a/spec/frontend/packages/list/components/packages_list_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_spec.js
@@ -18,13 +18,12 @@ describe('packages_list', () => {
let wrapper;
let store;
- const GlSortingItem = { name: 'sorting-item-stub', template: '<div><slot></slot></div>' };
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
const findPackageListPagination = () => wrapper.find(GlPagination);
const findPackageListDeleteModal = () => wrapper.find(GlModal);
- const findEmptySlot = () => wrapper.find({ name: 'empty-slot-stub' });
+ const findEmptySlot = () => wrapper.find(EmptySlotStub);
const findPackagesListRow = () => wrapper.find(PackagesListRow);
const createStore = (isGroupPage, packages, isLoading) => {
@@ -67,7 +66,6 @@ describe('packages_list', () => {
stubs: {
...stubChildren(PackagesList),
GlTable,
- GlSortingItem,
GlModal,
},
...options,
diff --git a/spec/frontend/packages/list/components/packages_sort_spec.js b/spec/frontend/packages/list/components/packages_sort_spec.js
index ff3e8e19413..5c4794d8f63 100644
--- a/spec/frontend/packages/list/components/packages_sort_spec.js
+++ b/spec/frontend/packages/list/components/packages_sort_spec.js
@@ -1,5 +1,5 @@
import Vuex from 'vuex';
-import { GlSorting } from '@gitlab/ui';
+import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import PackagesSort from '~/packages/list/components/packages_sort.vue';
@@ -13,8 +13,6 @@ describe('packages_sort', () => {
let sorting;
let sortingItems;
- const GlSortingItem = { name: 'sorting-item-stub', template: '<div><slot></slot></div>' };
-
const findPackageListSorting = () => wrapper.find(GlSorting);
const findSortingItems = () => wrapper.findAll(GlSortingItem);
diff --git a/spec/frontend/packages/list/stores/actions_spec.js b/spec/frontend/packages/list/stores/actions_spec.js
index faa629cc01f..cf205ecbac4 100644
--- a/spec/frontend/packages/list/stores/actions_spec.js
+++ b/spec/frontend/packages/list/stores/actions_spec.js
@@ -5,7 +5,8 @@ import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/packages/list/stores/actions';
import * as types from '~/packages/list/stores/mutation_types';
-import { MISSING_DELETE_PATH_ERROR, DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/list/constants';
+import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants';
+import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
jest.mock('~/flash.js');
jest.mock('~/api.js');
diff --git a/spec/frontend/packages/mock_data.js b/spec/frontend/packages/mock_data.js
index 86205b0744c..b95d06428ff 100644
--- a/spec/frontend/packages/mock_data.js
+++ b/spec/frontend/packages/mock_data.js
@@ -30,6 +30,7 @@ export const mavenPackage = {
name: 'Test package',
package_type: 'maven',
project_path: 'foo/bar/baz',
+ projectPathName: 'foo/bar/baz',
project_id: 1,
updated_at: '2015-12-10',
version: '1.0.0',
@@ -59,6 +60,7 @@ export const npmPackage = {
name: '@Test/package',
package_type: 'npm',
project_path: 'foo/bar/baz',
+ projectPathName: 'foo/bar/baz',
project_id: 1,
updated_at: '2015-12-10',
version: '',
@@ -86,6 +88,7 @@ export const conanPackage = {
id: 3,
name: 'conan-package',
project_path: 'foo/bar/baz',
+ projectPathName: 'foo/bar/baz',
package_files: [],
package_type: 'conan',
project_id: 1,
diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
index eab8d7b67cc..6aaefed92d0 100644
--- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
@@ -2,99 +2,143 @@
exports[`packages_list_row renders 1`] = `
<div
- class="gl-responsive-table-row"
- data-qa-selector="packages-row"
+ class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
+ data-qa-selector="package_row"
>
<div
- class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap"
+ class="gl-display-flex gl-align-items-center gl-py-5"
>
- <div
- class="d-flex align-items-center mr-2"
- >
- <gl-link-stub
- class="text-dark font-weight-bold mb-md-1"
- data-qa-selector="package_link"
- href="foo"
- >
-
- Test package
-
- </gl-link-stub>
-
- <!---->
- </div>
+ <!---->
<div
- class="d-flex text-secondary text-truncate mt-md-2"
+ class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
- <span>
- 1.0.0
- </span>
-
- <!---->
-
<div
- class="d-flex align-items-center"
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
>
- <gl-icon-stub
- class="text-secondary ml-2 mr-1"
- name="review-list"
- size="16"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
+ >
+ <gl-link-stub
+ class="gl-text-body gl-min-w-0"
+ data-qa-selector="package_link"
+ href="foo"
+ >
+ <gl-truncate-stub
+ position="end"
+ text="Test package"
+ />
+ </gl-link-stub>
+
+ <!---->
+ </div>
+
+ <!---->
+ </div>
- <gl-link-stub
- class="text-secondary"
- data-testid="packages-row-project"
- href="/foo/bar/baz"
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
-
- </gl-link-stub>
+ <div
+ class="gl-display-flex"
+ >
+ <span>
+ 1.0.0
+ </span>
+
+ <!---->
+
+ <div
+ class="gl-display-flex gl-align-items-center"
+ >
+ <gl-icon-stub
+ class="gl-ml-3 gl-mr-2 gl-min-w-0"
+ name="review-list"
+ size="16"
+ />
+
+ <gl-link-stub
+ class="gl-text-body gl-min-w-0"
+ data-testid="packages-row-project"
+ href="/foo/bar/baz"
+ >
+ <gl-truncate-stub
+ position="end"
+ text="foo/bar/baz"
+ />
+ </gl-link-stub>
+ </div>
+
+ <div
+ class="d-flex align-items-center"
+ data-testid="package-type"
+ >
+ <gl-icon-stub
+ class="gl-ml-3 gl-mr-2"
+ name="package"
+ size="16"
+ />
+
+ <span>
+ Maven
+ </span>
+ </div>
+ </div>
+ </div>
</div>
<div
- class="d-flex align-items-center"
- data-testid="package-type"
+ class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
>
- <gl-icon-stub
- class="text-secondary ml-2 mr-1"
- name="package"
- size="16"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
+ >
+ <publish-method-stub
+ packageentity="[object Object]"
+ />
+ </div>
- <span>
- Maven
- </span>
+ <div
+ class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ >
+ <span>
+ <gl-sprintf-stub
+ message="Created %{timestamp}"
+ />
+ </span>
+ </div>
</div>
</div>
- </div>
-
- <div
- class="table-section d-flex flex-md-column justify-content-between align-items-md-end section-40"
- >
- <publish-method-stub
- packageentity="[object Object]"
- />
<div
- class="text-secondary order-0 order-md-1 mt-md-2"
+ class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
>
- <gl-sprintf-stub
- message="Created %{timestamp}"
+ <gl-button-stub
+ aria-label="Remove package"
+ category="primary"
+ data-testid="action-delete"
+ icon="remove"
+ size="medium"
+ title="Remove package"
+ variant="danger"
/>
</div>
</div>
<div
- class="table-section section-10 d-flex justify-content-end"
+ class="gl-display-flex"
>
- <gl-button-stub
- aria-label="Remove package"
- category="primary"
- data-testid="action-delete"
- icon="remove"
- size="medium"
- title="Remove package"
- variant="danger"
+ <div
+ class="gl-w-7"
+ />
+
+ <!---->
+
+ <div
+ class="gl-w-9"
/>
</div>
</div>
diff --git a/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap
index 5ecca63d41d..9a0c52cee47 100644
--- a/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap
+++ b/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap
@@ -2,35 +2,37 @@
exports[`publish_method renders 1`] = `
<div
- class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1"
+ class="gl-display-flex gl-align-items-center"
>
<gl-icon-stub
- class="mr-1"
+ class="gl-mr-2"
name="git-merge"
size="16"
/>
- <strong
- class="mr-1 text-dark"
+ <span
+ class="gl-mr-2"
+ data-testid="pipeline-ref"
>
branch-name
- </strong>
+ </span>
<gl-icon-stub
- class="mr-1"
+ class="gl-mr-2"
name="commit"
size="16"
/>
<gl-link-stub
- class="mr-1"
+ class="gl-mr-2"
+ data-testid="pipeline-sha"
href="../commit/sha-baz"
>
sha-baz
</gl-link-stub>
<clipboard-button-stub
- cssclass="border-0 text-secondary py-0 px-1"
+ cssclass="gl-border-0 gl-py-0 gl-px-2"
text="sha-baz"
title="Copy commit SHA"
tooltipplacement="top"
diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js
index c0ae972d519..f4eabf7bb67 100644
--- a/spec/frontend/packages/shared/components/package_list_row_spec.js
+++ b/spec/frontend/packages/shared/components/package_list_row_spec.js
@@ -1,6 +1,7 @@
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
import PackageTags from '~/packages/shared/components/package_tags.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageList } from '../../mock_data';
describe('packages_list_row', () => {
@@ -17,14 +18,12 @@ describe('packages_list_row', () => {
const mountComponent = ({
isGroup = false,
packageEntity = packageWithoutTags,
- shallow = true,
showPackageType = true,
disableDelete = false,
} = {}) => {
- const mountFunc = shallow ? shallowMount : mount;
-
- wrapper = mountFunc(PackagesListRow, {
+ wrapper = shallowMount(PackagesListRow, {
store,
+ stubs: { ListItem },
propsData: {
packageLink: 'foo',
packageEntity,
@@ -92,15 +91,14 @@ describe('packages_list_row', () => {
});
describe('delete event', () => {
- beforeEach(() => mountComponent({ packageEntity: packageWithoutTags, shallow: false }));
+ beforeEach(() => mountComponent({ packageEntity: packageWithoutTags }));
- it('emits the packageToDelete event when the delete button is clicked', () => {
- findDeleteButton().trigger('click');
+ it('emits the packageToDelete event when the delete button is clicked', async () => {
+ findDeleteButton().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted('packageToDelete')).toBeTruthy();
- expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.emitted('packageToDelete')).toBeTruthy();
+ expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
});
});
diff --git a/spec/frontend/packages/shared/components/packages_list_loader_spec.js b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
index c8c2e2a4ba4..115a3a7095d 100644
--- a/spec/frontend/packages/shared/components/packages_list_loader_spec.js
+++ b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
@@ -12,8 +12,8 @@ describe('PackagesListLoader', () => {
});
};
- const getShapes = () => wrapper.vm.desktopShapes;
- const findSquareButton = () => wrapper.find({ ref: 'button-loader' });
+ const findDesktopShapes = () => wrapper.find('[data-testid="desktop-loader"]');
+ const findMobileShapes = () => wrapper.find('[data-testid="mobile-loader"]');
beforeEach(createComponent);
@@ -22,21 +22,30 @@ describe('PackagesListLoader', () => {
wrapper = null;
});
- describe('when used for projects', () => {
- it('should return 5 rects with last one being a square', () => {
- expect(getShapes()).toHaveLength(5);
- expect(findSquareButton().exists()).toBe(true);
+ describe('desktop loader', () => {
+ it('produces the right loader', () => {
+ expect(findDesktopShapes().findAll('rect[width="1000"]')).toHaveLength(20);
+ });
+
+ it('has the correct classes', () => {
+ expect(findDesktopShapes().classes()).toEqual([
+ 'gl-display-none',
+ 'gl-display-sm-flex',
+ 'gl-flex-direction-column',
+ ]);
});
});
- describe('when used for groups', () => {
- beforeEach(() => {
- createComponent({ isGroup: true });
+ describe('mobile loader', () => {
+ it('produces the right loader', () => {
+ expect(findMobileShapes().findAll('rect[height="170"]')).toHaveLength(5);
});
- it('should return 5 rects with no square', () => {
- expect(getShapes()).toHaveLength(5);
- expect(findSquareButton().exists()).toBe(false);
+ it('has the correct classes', () => {
+ expect(findMobileShapes().classes()).toEqual([
+ 'gl-flex-direction-column',
+ 'gl-display-sm-none',
+ ]);
});
});
});
diff --git a/spec/frontend/packages/shared/components/publish_method_spec.js b/spec/frontend/packages/shared/components/publish_method_spec.js
index bb9287c1204..6014774990c 100644
--- a/spec/frontend/packages/shared/components/publish_method_spec.js
+++ b/spec/frontend/packages/shared/components/publish_method_spec.js
@@ -7,9 +7,9 @@ describe('publish_method', () => {
const [packageWithoutPipeline, packageWithPipeline] = packageList;
- const findPipelineRef = () => wrapper.find({ ref: 'pipeline-ref' });
- const findPipelineSha = () => wrapper.find({ ref: 'pipeline-sha' });
- const findManualPublish = () => wrapper.find({ ref: 'manual-ref' });
+ const findPipelineRef = () => wrapper.find('[data-testid="pipeline-ref"]');
+ const findPipelineSha = () => wrapper.find('[data-testid="pipeline-sha"]');
+ const findManualPublish = () => wrapper.find('[data-testid="manually-published"]');
const mountComponent = (packageEntity = {}, isGroup = false) => {
wrapper = shallowMount(PublishMethod, {
diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
index fc37a545511..2fbc700d4f5 100644
--- a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
@@ -3,14 +3,15 @@
exports[`User Operation confirmation modal renders modal with form included 1`] = `
<div>
<p>
- content
+ <gl-sprintf-stub
+ message="content"
+ />
</p>
<p>
- To confirm, type
- <code>
- username
- </code>
+ <gl-sprintf-stub
+ message="To confirm, type %{username}"
+ />
</p>
<form
diff --git a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
index b3a297ac2c5..fbe2274c40d 100644
--- a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
+++ b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
+import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import CustomizeHomepageBanner from '~/pages/dashboard/projects/index/components/customize_homepage_banner.vue';
import axios from '~/lib/utils/axios_utils';
@@ -10,18 +11,22 @@ const provide = {
preferencesBehaviorPath: 'some/behavior/path',
calloutsPath: 'call/out/path',
calloutsFeatureId: 'some-feature-id',
+ trackLabel: 'home_page',
};
const createComponent = () => {
- return shallowMount(CustomizeHomepageBanner, { provide });
+ return shallowMount(CustomizeHomepageBanner, { provide, stubs: { GlBanner } });
};
describe('CustomizeHomepageBanner', () => {
+ let trackingSpy;
let mockAxios;
let wrapper;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
+ document.body.dataset.page = 'some:page';
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
wrapper = createComponent();
});
@@ -29,22 +34,75 @@ describe('CustomizeHomepageBanner', () => {
wrapper.destroy();
wrapper = null;
mockAxios.restore();
+ unmockTracking();
});
it('should render the banner when not dismissed', () => {
- expect(wrapper.contains(GlBanner)).toBe(true);
+ expect(wrapper.find(GlBanner).exists()).toBe(true);
});
it('should close the banner when dismiss is clicked', async () => {
mockAxios.onPost(provide.calloutsPath).replyOnce(200);
- expect(wrapper.contains(GlBanner)).toBe(true);
+ expect(wrapper.find(GlBanner).exists()).toBe(true);
wrapper.find(GlBanner).vm.$emit('close');
await wrapper.vm.$nextTick();
- expect(wrapper.contains(GlBanner)).toBe(false);
+ expect(wrapper.find(GlBanner).exists()).toBe(false);
});
it('includes the body text from options', () => {
expect(wrapper.html()).toContain(wrapper.vm.$options.i18n.body);
});
+
+ describe('tracking', () => {
+ const preferencesTrackingEvent = 'click_go_to_preferences';
+ const mockTrackingOnWrapper = () => {
+ unmockTracking();
+ trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
+ };
+
+ it('sets the needed data attributes for tracking button', async () => {
+ await wrapper.vm.$nextTick();
+ const button = wrapper.find(`[href='${wrapper.vm.preferencesBehaviorPath}']`);
+
+ expect(button.attributes('data-track-event')).toEqual(preferencesTrackingEvent);
+ expect(button.attributes('data-track-label')).toEqual(provide.trackLabel);
+ });
+
+ it('sends a tracking event when the banner is shown', () => {
+ const trackCategory = undefined;
+ const trackEvent = 'show_home_page_banner';
+
+ expect(trackingSpy).toHaveBeenCalledWith(trackCategory, trackEvent, {
+ label: provide.trackLabel,
+ });
+ });
+
+ it('sends a tracking event when the banner is dismissed', async () => {
+ mockTrackingOnWrapper();
+ mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ const trackCategory = undefined;
+ const trackEvent = 'click_dismiss';
+
+ wrapper.find(GlBanner).vm.$emit('close');
+
+ await wrapper.vm.$nextTick();
+ expect(trackingSpy).toHaveBeenCalledWith(trackCategory, trackEvent, {
+ label: provide.trackLabel,
+ });
+ });
+
+ it('sends a tracking event when the button is clicked', async () => {
+ mockTrackingOnWrapper();
+ mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ const button = wrapper.find(`[href='${wrapper.vm.preferencesBehaviorPath}']`);
+
+ triggerEvent(button.element);
+
+ await wrapper.vm.$nextTick();
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', preferencesTrackingEvent, {
+ label: provide.trackLabel,
+ });
+ });
+ });
});
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 204fe3d0a68..5ecb7860103 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils';
-import '~/gl_dropdown';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
diff --git a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
index 0bb96ee33d4..67ace608127 100644
--- a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
+++ b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
@@ -36,7 +36,7 @@ describe('BitbucketServerStatusTable', () => {
it('renders bitbucket status table component', () => {
createComponent();
- expect(wrapper.contains(BitbucketStatusTable)).toBe(true);
+ expect(wrapper.find(BitbucketStatusTable).exists()).toBe(true);
});
it('renders Reconfigure button', async () => {
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
index 2ec608569e3..9993e4da980 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
@@ -70,7 +70,7 @@ describe('Fork groups list component', () => {
replyWith(() => new Promise(() => {}));
createWrapper();
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('displays empty text if no groups are available', async () => {
@@ -89,7 +89,7 @@ describe('Fork groups list component', () => {
await waitForPromises();
- expect(wrapper.contains(GlSearchBoxByType)).toBe(true);
+ expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
});
it('renders list items for each available group', async () => {
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 54a080fb62b..8884f7815ab 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -124,7 +124,7 @@ describe('Code Coverage', () => {
});
it('renders the dropdown with all custom names as options', () => {
- expect(wrapper.contains(GlDeprecatedDropdown)).toBeDefined();
+ expect(wrapper.find(GlDeprecatedDropdown).exists()).toBeDefined();
expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
});
@@ -150,7 +150,11 @@ describe('Code Coverage', () => {
.find(GlIcon)
.exists(),
).toBe(false);
- expect(findSecondDropdownItem().contains(GlIcon)).toBe(true);
+ expect(
+ findSecondDropdownItem()
+ .find(GlIcon)
+ .exists(),
+ ).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 4c73225b54c..5efcedf678b 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import '~/gl_dropdown';
import TimezoneDropdown, {
formatUtcOffset,
formatTimezone,
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
index 8ab5426a005..1fd9d285610 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
@@ -27,14 +27,14 @@ describe('Project Feature Settings', () => {
describe('Hidden name input', () => {
it('should set the hidden name input if the name exists', () => {
- expect(wrapper.find({ name: 'Test' }).props().value).toBe(1);
+ expect(wrapper.find(`input[name=${defaultProps.name}]`).attributes().value).toBe('1');
});
it('should not set the hidden name input if the name does not exist', () => {
wrapper.setProps({ name: null });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ name: 'Test' }).exists()).toBe(false);
+ expect(wrapper.find(`input[name=${defaultProps.name}]`).exists()).toBe(false);
});
});
});
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 a50ceed5d09..e760cead760 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
@@ -40,7 +40,7 @@ const defaultProps = {
pagesAvailable: true,
pagesAccessControlEnabled: false,
pagesAccessControlForced: false,
- pagesHelpPath: '/help/user/project/pages/introduction#gitlab-pages-access-control-core',
+ pagesHelpPath: '/help/user/project/pages/introduction#gitlab-pages-access-control',
packagesAvailable: false,
packagesHelpPath: '/help/user/packages/index',
};
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index b9dc4c9588c..ff51b1184cb 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -30,7 +30,7 @@ describe('detailedMetric', () => {
});
it('does not render the element', () => {
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
});
});
diff --git a/spec/frontend/performance_bar/components/request_warning_spec.js b/spec/frontend/performance_bar/components/request_warning_spec.js
index 21f7bdf01f3..d558c7b018a 100644
--- a/spec/frontend/performance_bar/components/request_warning_spec.js
+++ b/spec/frontend/performance_bar/components/request_warning_spec.js
@@ -27,7 +27,7 @@ describe('request warning', () => {
});
it('does nothing', () => {
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
});
});
});
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 621c7d87a7e..1517142c21e 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -44,7 +44,7 @@ describe('performance bar wrapper', () => {
{},
);
- vm = performanceBar({ container: '#js-peek' });
+ vm = performanceBar(peekWrapper);
});
afterEach(() => {
diff --git a/spec/frontend/performance_bar/services/performance_bar_service_spec.js b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
index cfec4b779e4..36bfd575c12 100644
--- a/spec/frontend/performance_bar/services/performance_bar_service_spec.js
+++ b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
@@ -9,18 +9,12 @@ describe('PerformanceBarService', () => {
it('returns false when the request URL is the peek URL', () => {
expect(
- fireCallback({ headers: { 'x-request-id': '123' }, url: '/peek' }, '/peek'),
- ).toBeFalsy();
+ fireCallback({ headers: { 'x-request-id': '123' }, config: { url: '/peek' } }, '/peek'),
+ ).toBe(false);
});
it('returns false when there is no request ID', () => {
- expect(fireCallback({ headers: {}, url: '/request' }, '/peek')).toBeFalsy();
- });
-
- it('returns false when the request is an API request', () => {
- expect(
- fireCallback({ headers: { 'x-request-id': '123' }, url: '/api/' }, '/peek'),
- ).toBeFalsy();
+ expect(fireCallback({ headers: {}, config: { url: '/request' } }, '/peek')).toBe(false);
});
it('returns false when the response is from the cache', () => {
@@ -29,13 +23,22 @@ describe('PerformanceBarService', () => {
{ headers: { 'x-request-id': '123', 'x-gitlab-from-cache': 'true' }, url: '/request' },
'/peek',
),
- ).toBeFalsy();
+ ).toBe(false);
+ });
+
+ it('returns true when the request is an API request', () => {
+ expect(
+ fireCallback({ headers: { 'x-request-id': '123' }, config: { url: '/api/' } }, '/peek'),
+ ).toBe(true);
});
- it('returns true when all conditions are met', () => {
+ it('returns true for all other requests', () => {
expect(
- fireCallback({ headers: { 'x-request-id': '123' }, url: '/request' }, '/peek'),
- ).toBeTruthy();
+ fireCallback(
+ { headers: { 'x-request-id': '123' }, config: { url: '/request' } },
+ '/peek',
+ ),
+ ).toBe(true);
});
});
@@ -45,7 +48,7 @@ describe('PerformanceBarService', () => {
}
it('gets the request ID from the headers', () => {
- expect(requestId({ headers: { 'x-request-id': '123' } }, '/peek')).toEqual('123');
+ expect(requestId({ headers: { 'x-request-id': '123' } }, '/peek')).toBe('123');
});
});
@@ -54,14 +57,10 @@ describe('PerformanceBarService', () => {
return PerformanceBarService.callbackParams(response, peekUrl)[2];
}
- it('gets the request URL from the response object', () => {
- expect(requestUrl({ headers: {}, url: '/request' }, '/peek')).toEqual('/request');
- });
-
- it('gets the request URL from response.config if present', () => {
- expect(
- requestUrl({ headers: {}, config: { url: '/config-url' }, url: '/request' }, '/peek'),
- ).toEqual('/config-url');
+ it('gets the request URL from response.config', () => {
+ expect(requestUrl({ headers: {}, config: { url: '/config-url' } }, '/peek')).toBe(
+ '/config-url',
+ );
});
});
});
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 d1e6b6b938a..97a92778f1a 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -1,31 +1,48 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { GlNewDropdown, GlNewDropdownItem, GlForm } from '@gitlab/ui';
-import Api from '~/api';
+import { GlDropdown, GlDropdownItem, GlForm, GlSprintf } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
-import { mockRefs, mockParams, mockPostParams, mockProjectId } from '../mock_data';
+import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data';
+import { redirectTo } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+}));
+
+const pipelinesPath = '/root/project/-/pipleines';
+const postResponse = { id: 1 };
describe('Pipeline New Form', () => {
let wrapper;
+ let mock;
const dummySubmitEvent = {
preventDefault() {},
};
const findForm = () => wrapper.find(GlForm);
- const findDropdown = () => wrapper.find(GlNewDropdown);
- const findDropdownItems = () => wrapper.findAll(GlNewDropdownItem);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
+ const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
+ const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
+ const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
+ const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
+ const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data);
const createComponent = (term = '', props = {}, method = shallowMount) => {
wrapper = method(PipelineNewForm, {
propsData: {
projectId: mockProjectId,
- pipelinesPath: '',
+ pipelinesPath,
refs: mockRefs,
defaultBranch: 'master',
settingsLink: '',
+ maxWarnings: 25,
...props,
},
data() {
@@ -37,24 +54,30 @@ describe('Pipeline New Form', () => {
};
beforeEach(() => {
- jest.spyOn(Api, 'createPipeline').mockResolvedValue({ data: { web_url: '/' } });
+ mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+
+ mock.restore();
});
describe('Dropdown with branches and tags', () => {
+ beforeEach(() => {
+ mock.onPost(pipelinesPath).reply(200, postResponse);
+ });
+
it('displays dropdown with all branches and tags', () => {
createComponent();
- expect(findDropdownItems().length).toBe(mockRefs.length);
+ expect(findDropdownItems()).toHaveLength(mockRefs.length);
});
it('when user enters search term the list is filtered', () => {
createComponent('master');
- expect(findDropdownItems().length).toBe(1);
+ expect(findDropdownItems()).toHaveLength(1);
expect(
findDropdownItems()
.at(0)
@@ -66,43 +89,78 @@ describe('Pipeline New Form', () => {
describe('Form', () => {
beforeEach(() => {
createComponent('', mockParams, mount);
+
+ mock.onPost(pipelinesPath).reply(200, postResponse);
});
- it('displays the correct values for the provided query params', () => {
+ it('displays the correct values for the provided query params', async () => {
expect(findDropdown().props('text')).toBe('tag-1');
- return wrapper.vm.$nextTick().then(() => {
- expect(findVariableRows().length).toBe(3);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findVariableRows()).toHaveLength(3);
});
it('does not display remove icon for last row', () => {
- expect(findRemoveIcons().length).toBe(2);
+ expect(findRemoveIcons()).toHaveLength(2);
});
- it('removes ci variable row on remove icon button click', () => {
+ it('removes ci variable row on remove icon button click', async () => {
findRemoveIcons()
.at(1)
.trigger('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findVariableRows().length).toBe(2);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findVariableRows()).toHaveLength(2);
});
- it('creates a pipeline on submit', () => {
+ it('creates a pipeline on submit', async () => {
findForm().vm.$emit('submit', dummySubmitEvent);
- expect(Api.createPipeline).toHaveBeenCalledWith(mockProjectId, mockPostParams);
+ await waitForPromises();
+
+ expect(getExpectedPostParams()).toEqual(mockPostParams);
+ expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
});
- it('creates blank variable on input change event', () => {
+ it('creates blank variable on input change event', async () => {
findKeyInputs()
.at(2)
.trigger('change');
- return wrapper.vm.$nextTick().then(() => {
- expect(findVariableRows().length).toBe(4);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findVariableRows()).toHaveLength(4);
+ });
+ });
+
+ describe('Form errors and warnings', () => {
+ beforeEach(() => {
+ createComponent();
+
+ mock.onPost(pipelinesPath).reply(400, mockError);
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ return waitForPromises();
+ });
+
+ it('shows both error and warning', () => {
+ expect(findErrorAlert().exists()).toBe(true);
+ expect(findWarningAlert().exists()).toBe(true);
+ });
+
+ it('shows the correct error', () => {
+ expect(findErrorAlert().text()).toBe(mockError.errors[0]);
+ });
+
+ it('shows the correct warning title', () => {
+ const { length } = mockError.warnings;
+ expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
+ });
+
+ it('shows the correct amount of warnings', () => {
+ expect(findWarnings()).toHaveLength(mockError.warnings.length);
});
});
});
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js
index 55ec1fb5afc..55286e0ec7e 100644
--- a/spec/frontend/pipeline_new/mock_data.js
+++ b/spec/frontend/pipeline_new/mock_data.js
@@ -19,3 +19,15 @@ export const mockPostParams = {
{ key: 'test_file', value: 'test_file_val', variable_type: 'file' },
],
};
+
+export const mockError = {
+ errors: [
+ 'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post',
+ ],
+ warnings: [
+ 'jobs:build1 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
+ 'jobs:build2 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
+ 'jobs:build3 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
+ ],
+ total_warnings: 7,
+};
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index c5b7318d3af..8a6586a7d7d 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -47,9 +47,6 @@ describe('Pipelines filtered search', () => {
});
it('displays UI elements', () => {
- expect(wrapper.isVueInstance()).toBe(true);
- expect(wrapper.isEmpty()).toBe(false);
-
expect(findFilteredSearch().exists()).toBe(true);
});
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 1389649abea..d977db58a0e 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -16,6 +16,9 @@ describe('graph component', () => {
let wrapper;
+ const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]');
+ const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]');
+
beforeEach(() => {
setHTMLFixture('<div class="layout-page"></div>');
});
@@ -167,7 +170,7 @@ describe('graph component', () => {
describe('triggered by', () => {
describe('on click', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
- const btnWrapper = wrapper.find('.linked-pipeline-content');
+ const btnWrapper = findExpandPipelineBtn();
btnWrapper.trigger('click');
@@ -213,7 +216,7 @@ describe('graph component', () => {
),
});
- const btnWrappers = wrapper.findAll('.linked-pipeline-content');
+ const btnWrappers = findAllExpandPipelineBtns();
const downstreamBtnWrapper = btnWrappers.at(btnWrappers.length - 1);
downstreamBtnWrapper.trigger('click');
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 2c5e7a1f6e9..e844cbc5bf8 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -6,6 +6,7 @@ describe('pipeline graph job item', () => {
let wrapper;
const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
+ const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]');
const createWrapper = propsData => {
wrapper = mount(JobItem, {
@@ -13,6 +14,7 @@ describe('pipeline graph job item', () => {
});
};
+ const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const mockJob = {
id: 4256,
@@ -33,6 +35,18 @@ describe('pipeline graph job item', () => {
},
},
};
+ const mockJobWithoutDetails = {
+ id: 4257,
+ name: 'job_without_details',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ details_path: '/root/ci-mock/builds/4257',
+ has_details: false,
+ },
+ };
afterEach(() => {
wrapper.destroy();
@@ -47,7 +61,7 @@ describe('pipeline graph job item', () => {
expect(link.attributes('href')).toBe(mockJob.status.details_path);
- expect(link.attributes('title')).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
+ expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
@@ -61,18 +75,7 @@ describe('pipeline graph job item', () => {
describe('name without link', () => {
beforeEach(() => {
createWrapper({
- job: {
- id: 4257,
- name: 'test',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- details_path: '/root/ci-mock/builds/4257',
- has_details: false,
- },
- },
+ job: mockJobWithoutDetails,
cssClassJobName: 'css-class-job-name',
jobHovered: 'test',
});
@@ -82,11 +85,10 @@ describe('pipeline graph job item', () => {
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
expect(wrapper.find('a').exists()).toBe(false);
- expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name);
+ expect(trimText(wrapper.find('.ci-status-text').text())).toBe(mockJobWithoutDetails.name);
});
it('should apply hover class and provided class name', () => {
- expect(findJobWithoutLink().classes()).toContain('gl-inset-border-1-blue-500');
expect(findJobWithoutLink().classes()).toContain('css-class-job-name');
});
});
@@ -137,9 +139,7 @@ describe('pipeline graph job item', () => {
},
});
- expect(wrapper.find('.js-job-component-tooltip').attributes('title')).toEqual(
- 'test - success',
- );
+ expect(wrapper.find('.js-job-component-tooltip').attributes('title')).toBe('test - success');
});
});
@@ -149,9 +149,39 @@ describe('pipeline graph job item', () => {
job: delayedJobFixture,
});
- expect(wrapper.find('.js-pipeline-graph-job-link').attributes('title')).toEqual(
+ expect(findJobWithLink().attributes('title')).toBe(
`delayed job - delayed manual action (${wrapper.vm.remainingTime})`,
);
});
});
+
+ describe('trigger job highlighting', () => {
+ it.each`
+ job | jobName | expanded | link
+ ${mockJob} | ${mockJob.name} | ${true} | ${true}
+ ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${true} | ${false}
+ `(
+ `trigger job should stay highlighted when downstream is expanded`,
+ ({ job, jobName, expanded, link }) => {
+ createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ const findJobEl = link ? findJobWithLink : findJobWithoutLink;
+
+ expect(findJobEl().classes()).toContain(triggerActiveClass);
+ },
+ );
+
+ it.each`
+ job | jobName | expanded | link
+ ${mockJob} | ${mockJob.name} | ${false} | ${true}
+ ${mockJobWithoutDetails} | ${mockJobWithoutDetails.name} | ${false} | ${false}
+ `(
+ `trigger job should not be highlighted when downstream is not expanded`,
+ ({ job, jobName, expanded, link }) => {
+ createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ const findJobEl = link ? findJobWithLink : findJobWithoutLink;
+
+ expect(findJobEl().classes()).not.toContain(triggerActiveClass);
+ },
+ );
+ });
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 59121c54ff3..8e65f0d4f71 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
@@ -16,10 +16,18 @@ describe('Linked pipeline', () => {
const findButton = () => wrapper.find(GlButton);
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]');
+ const findExpandButton = () => wrapper.find('[data-testid="expandPipelineButton"]');
- const createWrapper = propsData => {
+ const createWrapper = (propsData, data = []) => {
wrapper = mount(LinkedPipelineComponent, {
propsData,
+ data() {
+ return {
+ ...data,
+ };
+ },
});
};
@@ -39,7 +47,7 @@ describe('Linked pipeline', () => {
});
it('should render a list item as the containing element', () => {
- expect(wrapper.is('li')).toBe(true);
+ expect(wrapper.element.tagName).toBe('LI');
});
it('should render a button', () => {
@@ -76,7 +84,7 @@ describe('Linked pipeline', () => {
});
it('should render the tooltip text as the title attribute', () => {
- const titleAttr = findButton().attributes('title');
+ const titleAttr = findLinkedPipeline().attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.details.status.label);
@@ -117,6 +125,56 @@ describe('Linked pipeline', () => {
createWrapper(upstreamProps);
expect(findPipelineLabel().exists()).toBe(true);
});
+
+ it('downstream pipeline should contain the correct link', () => {
+ createWrapper(downstreamProps);
+ expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
+ });
+
+ it('upstream pipeline should contain the correct link', () => {
+ createWrapper(upstreamProps);
+ expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
+ });
+
+ it.each`
+ presentClass | missingClass
+ ${'gl-right-0'} | ${'gl-left-0'}
+ ${'gl-border-l-1!'} | ${'gl-border-r-1!'}
+ `(
+ 'pipeline expand button should be postioned right when child pipeline',
+ ({ presentClass, missingClass }) => {
+ createWrapper(downstreamProps);
+ expect(findExpandButton().classes()).toContain(presentClass);
+ expect(findExpandButton().classes()).not.toContain(missingClass);
+ },
+ );
+
+ it.each`
+ presentClass | missingClass
+ ${'gl-left-0'} | ${'gl-right-0'}
+ ${'gl-border-r-1!'} | ${'gl-border-l-1!'}
+ `(
+ 'pipeline expand button should be postioned left when parent pipeline',
+ ({ presentClass, missingClass }) => {
+ createWrapper(upstreamProps);
+ expect(findExpandButton().classes()).toContain(presentClass);
+ expect(findExpandButton().classes()).not.toContain(missingClass);
+ },
+ );
+
+ it.each`
+ pipelineType | anglePosition | expanded
+ ${downstreamProps} | ${'angle-right'} | ${false}
+ ${downstreamProps} | ${'angle-left'} | ${true}
+ ${upstreamProps} | ${'angle-left'} | ${false}
+ ${upstreamProps} | ${'angle-right'} | ${true}
+ `(
+ '$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded',
+ ({ pipelineType, anglePosition, expanded }) => {
+ createWrapper(pipelineType, { expanded });
+ expect(findExpandButton().props('icon')).toBe(anglePosition);
+ },
+ );
});
describe('when isLoading is true', () => {
@@ -130,8 +188,8 @@ describe('Linked pipeline', () => {
createWrapper(props);
});
- it('sets the loading prop to true', () => {
- expect(findButton().props('loading')).toBe(true);
+ it('loading icon is visible', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
});
});
@@ -172,5 +230,10 @@ describe('Linked pipeline', () => {
findLinkedPipeline().trigger('mouseleave');
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]);
});
+
+ it('should emit pipelineExpanded with job name and expanded state on click', () => {
+ findExpandButton().trigger('click');
+ expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['trigger_job', true]]);
+ });
});
});
diff --git a/spec/frontend/pipelines/pipeline_graph/gitlab_ci_yaml_visualization_spec.js b/spec/frontend/pipelines/pipeline_graph/gitlab_ci_yaml_visualization_spec.js
new file mode 100644
index 00000000000..fea42350959
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_graph/gitlab_ci_yaml_visualization_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlTab } from '@gitlab/ui';
+import { yamlString } from './mock_data';
+import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import GitlabCiYamlVisualization from '~/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue';
+
+describe('gitlab yaml visualization component', () => {
+ const defaultProps = { blobData: yamlString };
+ let wrapper;
+
+ const createComponent = props => {
+ return shallowMount(GitlabCiYamlVisualization, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findGlTabComponents = () => wrapper.findAll(GlTab);
+ const findPipelineGraph = () => wrapper.find(PipelineGraph);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('tabs component', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('renders the file and visualization tabs', () => {
+ expect(findGlTabComponents()).toHaveLength(2);
+ });
+ });
+
+ describe('graph component', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('is hidden by default', () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
new file mode 100644
index 00000000000..5a5d6c021a6
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -0,0 +1,80 @@
+export const yamlString = `stages:
+- empty
+- build
+- test
+- deploy
+- final
+
+include:
+- template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
+
+build_a:
+ stage: build
+ script: echo hello
+build_b:
+ stage: build
+ script: echo hello
+build_c:
+ stage: build
+ script: echo hello
+build_d:
+ stage: Queen
+ script: echo hello
+
+test_a:
+ stage: test
+ script: ls
+ needs: [build_a, build_b, build_c]
+test_b:
+ stage: test
+ script: ls
+ needs: [build_a, build_b, build_d]
+test_c:
+ stage: test
+ script: ls
+ needs: [build_a, build_b, build_c]
+
+deploy_a:
+ stage: deploy
+ script: echo hello
+`;
+
+export const pipelineData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [],
+ },
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test' }],
+ },
+ {
+ name: 'test_2',
+ jobs: [{ script: 'yarn karma', stage: 'test' }],
+ },
+ ],
+ },
+ {
+ name: 'deploy',
+ groups: [
+ {
+ name: 'deploy_1',
+ jobs: [{ script: 'yarn magick', stage: 'deploy' }],
+ },
+ ],
+ },
+ ],
+};
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
new file mode 100644
index 00000000000..30e192e5726
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import { pipelineData } from './mock_data';
+import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
+import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
+
+describe('pipeline graph component', () => {
+ const defaultProps = { pipelineData };
+ let wrapper;
+
+ const createComponent = props => {
+ return shallowMount(PipelineGraph, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findAllStagePills = () => wrapper.findAll(StagePill);
+ const findAllJobPills = () => wrapper.findAll(JobPill);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('with no data', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ pipelineData: {} });
+ });
+
+ it('renders an empty section', () => {
+ expect(wrapper.text()).toContain('No content to show');
+ expect(findAllStagePills()).toHaveLength(0);
+ expect(findAllJobPills()).toHaveLength(0);
+ });
+ });
+
+ describe('with data', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+ it('renders the right number of stage pills', () => {
+ const expectedStagesLength = pipelineData.stages.length;
+
+ expect(findAllStagePills()).toHaveLength(expectedStagesLength);
+ });
+
+ it('renders the right number of job pills', () => {
+ // We count the number of jobs in the mock data
+ const expectedJobsLength = pipelineData.stages.reduce((acc, val) => {
+ return acc + val.groups.length;
+ }, 0);
+
+ expect(findAllJobPills()).toHaveLength(expectedJobsLength);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
new file mode 100644
index 00000000000..dd85c8c2bd0
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
@@ -0,0 +1,150 @@
+import { preparePipelineGraphData } from '~/pipelines/utils';
+
+describe('preparePipelineGraphData', () => {
+ const emptyResponse = { stages: [] };
+ const jobName1 = 'build_1';
+ const jobName2 = 'build_2';
+ const jobName3 = 'test_1';
+ const jobName4 = 'deploy_1';
+ const job1 = { [jobName1]: { script: 'echo hello', stage: 'build' } };
+ const job2 = { [jobName2]: { script: 'echo build', stage: 'build' } };
+ const job3 = { [jobName3]: { script: 'echo test', stage: 'test' } };
+ const job4 = { [jobName4]: { script: 'echo deploy', stage: 'deploy' } };
+
+ describe('returns an object with an empty array of stages if', () => {
+ it('no data is passed', () => {
+ expect(preparePipelineGraphData({})).toEqual(emptyResponse);
+ });
+
+ it('no stages are found', () => {
+ expect(preparePipelineGraphData({ includes: 'template/myTemplate.gitlab-ci.yml' })).toEqual(
+ emptyResponse,
+ );
+ });
+ });
+
+ describe('returns the correct array of stages', () => {
+ it('when multiple jobs are in the same stage', () => {
+ const expectedData = {
+ stages: [
+ {
+ name: job1[jobName1].stage,
+ groups: [
+ {
+ name: jobName1,
+ jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
+ },
+ {
+ name: jobName2,
+ jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }],
+ },
+ ],
+ },
+ ],
+ };
+
+ expect(preparePipelineGraphData({ ...job1, ...job2 })).toEqual(expectedData);
+ });
+
+ it('when stages are defined by the user', () => {
+ const userDefinedStage = 'myStage';
+ const userDefinedStage2 = 'myStage2';
+
+ const expectedData = {
+ stages: [
+ {
+ name: userDefinedStage,
+ groups: [],
+ },
+ {
+ name: userDefinedStage2,
+ groups: [],
+ },
+ ],
+ };
+
+ expect(preparePipelineGraphData({ stages: [userDefinedStage, userDefinedStage2] })).toEqual(
+ expectedData,
+ );
+ });
+
+ it('by combining user defined stage and job stages, it preserves user defined order', () => {
+ const userDefinedStage = 'myStage';
+ const userDefinedStageThatOverlaps = 'deploy';
+
+ const expectedData = {
+ stages: [
+ {
+ name: userDefinedStage,
+ groups: [],
+ },
+ {
+ name: job4[jobName4].stage,
+ groups: [
+ {
+ name: jobName4,
+ jobs: [{ script: job4[jobName4].script, stage: job4[jobName4].stage }],
+ },
+ ],
+ },
+ {
+ name: job1[jobName1].stage,
+ groups: [
+ {
+ name: jobName1,
+ jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
+ },
+ {
+ name: jobName2,
+ jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }],
+ },
+ ],
+ },
+ {
+ name: job3[jobName3].stage,
+ groups: [
+ {
+ name: jobName3,
+ jobs: [{ script: job3[jobName3].script, stage: job3[jobName3].stage }],
+ },
+ ],
+ },
+ ],
+ };
+
+ expect(
+ preparePipelineGraphData({
+ stages: [userDefinedStage, userDefinedStageThatOverlaps],
+ ...job1,
+ ...job2,
+ ...job3,
+ ...job4,
+ }),
+ ).toEqual(expectedData);
+ });
+
+ it('with only unique values', () => {
+ const expectedData = {
+ stages: [
+ {
+ name: job1[jobName1].stage,
+ groups: [
+ {
+ name: jobName1,
+ jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
+ },
+ ],
+ },
+ ],
+ };
+
+ expect(
+ preparePipelineGraphData({
+ stages: ['build'],
+ ...job1,
+ ...job1,
+ }),
+ ).toEqual(expectedData);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index 6fd9a143d82..ad8136890e6 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -36,7 +36,7 @@ describe('Pipelines Triggerer', () => {
});
it('should render a table cell', () => {
- expect(wrapper.contains('.table-section')).toBe(true);
+ expect(wrapper.find('.table-section').exists()).toBe(true);
});
it('should pass triggerer information when triggerer is provided', () => {
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index cce4c2dfa7b..071a2b24889 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue';
@@ -19,7 +19,7 @@ describe('Pipelines Actions dropdown', () => {
});
};
- const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedButton);
+ const findAllDropdownItems = () => wrapper.findAll(GlButton);
const findAllCountdowns = () => wrapper.findAll(GlCountdown);
beforeEach(() => {
@@ -66,7 +66,7 @@ describe('Pipelines Actions dropdown', () => {
it('makes a request and toggles the loading state', () => {
mock.onPost(mockActions.path).reply(200);
- wrapper.find(GlDeprecatedButton).vm.$emit('click');
+ wrapper.find(GlButton).vm.$emit('click');
expect(wrapper.vm.isLoading).toBe(true);
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
index ca9ebb54138..58e8065033f 100644
--- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -1,6 +1,6 @@
import { getJSONFixture } from 'helpers/fixtures';
import * as getters from '~/pipelines/stores/test_reports/getters';
-import { iconForTestStatus } from '~/pipelines/stores/test_reports/utils';
+import { iconForTestStatus, formattedTime } from '~/pipelines/stores/test_reports/utils';
describe('Getters TestReports Store', () => {
let state;
@@ -34,7 +34,7 @@ describe('Getters TestReports Store', () => {
const suites = getters.getTestSuites(state);
const expected = testReports.test_suites.map(x => ({
...x,
- formattedTime: '00:00:00',
+ formattedTime: formattedTime(x.total_time),
}));
expect(suites).toEqual(expected);
@@ -65,7 +65,7 @@ describe('Getters TestReports Store', () => {
const cases = getters.getSuiteTests(state);
const expected = testReports.test_suites[0].test_cases.map(x => ({
...x,
- formattedTime: '00:00:00',
+ formattedTime: formattedTime(x.execution_time),
icon: iconForTestStatus(x.status),
}));
diff --git a/spec/frontend/pipelines/test_reports/stores/utils_spec.js b/spec/frontend/pipelines/test_reports/stores/utils_spec.js
new file mode 100644
index 00000000000..7e632d099fc
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/utils_spec.js
@@ -0,0 +1,26 @@
+import { formattedTime } from '~/pipelines/stores/test_reports/utils';
+
+describe('Test reports utils', () => {
+ describe('formattedTime', () => {
+ describe('when time is smaller than a second', () => {
+ it('should return time in milliseconds fixed to 2 decimals', () => {
+ const result = formattedTime(0.4815162342);
+ expect(result).toBe('481.52ms');
+ });
+ });
+
+ describe('when time is equal to a second', () => {
+ it('should return time in seconds fixed to 2 decimals', () => {
+ const result = formattedTime(1);
+ expect(result).toBe('1.00s');
+ });
+ });
+
+ describe('when time is greater than a second', () => {
+ it('should return time in seconds fixed to 2 decimals', () => {
+ const result = formattedTime(4.815162342);
+ expect(result).toBe('4.82s');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index a709edf5184..c8ab18b9086 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -1,4 +1,5 @@
import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
@@ -15,9 +16,9 @@ describe('Test reports app', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
- const loadingSpinner = () => wrapper.find('.js-loading-spinner');
- const testsDetail = () => wrapper.find('.js-tests-detail');
- const noTestsToShow = () => wrapper.find('.js-no-tests-to-show');
+ const loadingSpinner = () => wrapper.find(GlLoadingIcon);
+ const testsDetail = () => wrapper.find('[data-testid="tests-detail"]');
+ const noTestsToShow = () => wrapper.find('[data-testid="no-tests-to-show"]');
const testSummary = () => wrapper.find(TestSummary);
const testSummaryTable = () => wrapper.find(TestSummaryTable);
@@ -88,6 +89,10 @@ describe('Test reports app', () => {
expect(wrapper.vm.testReports).toBeTruthy();
expect(wrapper.vm.showTests).toBeTruthy();
});
+
+ it('shows tests details', () => {
+ expect(testsDetail().exists()).toBe(true);
+ });
});
describe('when a suite is clicked', () => {
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index 3a4aa94571e..2feb6aa5799 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -23,8 +23,6 @@ describe('Test reports suite table', () => {
const noCasesMessage = () => wrapper.find('.js-no-test-cases');
const allCaseRows = () => wrapper.findAll('.js-case-row');
const findCaseRowAtIndex = index => wrapper.findAll('.js-case-row').at(index);
- const allCaseNames = () =>
- wrapper.findAll('[data-testid="caseName"]').wrappers.map(el => el.attributes('text'));
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
const createComponent = (suite = testSuite) => {
@@ -63,16 +61,6 @@ describe('Test reports suite table', () => {
expect(allCaseRows().length).toBe(testCases.length);
});
- it('renders the failed tests first, skipped tests next, then successful tests', () => {
- const expectedCaseOrder = [
- ...testCases.filter(x => x.status === TestStatus.FAILED),
- ...testCases.filter(x => x.status === TestStatus.SKIPPED),
- ...testCases.filter(x => x.status === TestStatus.SUCCESS),
- ].map(x => x.name);
-
- expect(allCaseNames()).toEqual(expectedCaseOrder);
- });
-
it('renders the correct icon for each status', () => {
const failedTest = testCases.findIndex(x => x.status === TestStatus.FAILED);
const skippedTest = testCases.findIndex(x => x.status === TestStatus.SKIPPED);
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js
index 79be6c168cf..dc5af7b160c 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import Summary from '~/pipelines/components/test_reports/test_summary.vue';
+import { formattedTime } from '~/pipelines/stores/test_reports/utils';
describe('Test reports summary', () => {
let wrapper;
@@ -76,7 +77,7 @@ describe('Test reports summary', () => {
});
it('displays the correctly formatted duration', () => {
- expect(duration().text()).toBe('00:00:00');
+ expect(duration().text()).toBe(formattedTime(testSuite.total_time));
});
});
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index 04934fb93b0..b7bc8d08a0f 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
describe('Timeago component', () => {
@@ -22,14 +23,19 @@ describe('Timeago component', () => {
wrapper = null;
});
+ const duration = () => wrapper.find('.duration');
+ const finishedAt = () => wrapper.find('.finished-at');
+
describe('with duration', () => {
beforeEach(() => {
createComponent({ duration: 10, finishedTime: '' });
});
it('should render duration and timer svg', () => {
- expect(wrapper.find('.duration').exists()).toBe(true);
- expect(wrapper.find('.duration svg').exists()).toBe(true);
+ const icon = duration().find(GlIcon);
+
+ expect(duration().exists()).toBe(true);
+ expect(icon.props('name')).toBe('timer');
});
});
@@ -39,7 +45,7 @@ describe('Timeago component', () => {
});
it('should not render duration and timer svg', () => {
- expect(wrapper.find('.duration').exists()).toBe(false);
+ expect(duration().exists()).toBe(false);
});
});
@@ -49,9 +55,12 @@ describe('Timeago component', () => {
});
it('should render time and calendar icon', () => {
- expect(wrapper.find('.finished-at').exists()).toBe(true);
- expect(wrapper.find('.finished-at i.fa-calendar').exists()).toBe(true);
- expect(wrapper.find('.finished-at time').exists()).toBe(true);
+ const icon = finishedAt().find(GlIcon);
+ const time = finishedAt().find('time');
+
+ expect(finishedAt().exists()).toBe(true);
+ expect(icon.props('name')).toBe('calendar');
+ expect(time.exists()).toBe(true);
});
});
@@ -61,7 +70,7 @@ describe('Timeago component', () => {
});
it('should not render time and calendar icon', () => {
- expect(wrapper.find('.finished-at').exists()).toBe(false);
+ expect(finishedAt().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
index 096e4cd97f6..b53955ab743 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -5,16 +5,17 @@ import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pi
describe('Pipeline Status Token', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
- const findAllGlIcons = () => wrapper.findAll(GlIcon);
-
const stubs = {
GlFilteredSearchToken: {
+ props: GlFilteredSearchToken.props,
template: `<div><slot name="suggestions"></slot></div>`,
},
};
+ const findFilteredSearchToken = () => wrapper.find(stubs.GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findAllGlIcons = () => wrapper.findAll(GlIcon);
+
const defaultProps = {
config: {
type: 'status',
@@ -27,12 +28,12 @@ describe('Pipeline Status Token', () => {
},
};
- const createComponent = options => {
+ const createComponent = () => {
wrapper = shallowMount(PipelineStatusToken, {
propsData: {
...defaultProps,
},
- ...options,
+ stubs,
});
};
@@ -50,10 +51,6 @@ describe('Pipeline Status Token', () => {
});
describe('shows statuses correctly', () => {
- beforeEach(() => {
- createComponent({ stubs });
- });
-
it('renders all pipeline statuses available', () => {
expect(findAllFilteredSearchSuggestions()).toHaveLength(wrapper.vm.statuses.length);
expect(findAllGlIcons()).toHaveLength(wrapper.vm.statuses.length);
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
index c95d2ea1b7b..9363944a719 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -7,16 +7,17 @@ import { users } from '../mock_data';
describe('Pipeline Trigger Author Token', () => {
let wrapper;
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
-
const stubs = {
GlFilteredSearchToken: {
+ props: GlFilteredSearchToken.props,
template: `<div><slot name="suggestions"></slot></div>`,
},
};
+ const findFilteredSearchToken = () => wrapper.find(stubs.GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
const defaultProps = {
config: {
type: 'username',
@@ -31,7 +32,7 @@ describe('Pipeline Trigger Author Token', () => {
},
};
- const createComponent = (options, data) => {
+ const createComponent = data => {
wrapper = shallowMount(PipelineTriggerAuthorToken, {
propsData: {
...defaultProps,
@@ -41,7 +42,7 @@ describe('Pipeline Trigger Author Token', () => {
...data,
};
},
- ...options,
+ stubs,
});
};
@@ -69,13 +70,13 @@ describe('Pipeline Trigger Author Token', () => {
describe('displays loading icon correctly', () => {
it('shows loading icon', () => {
- createComponent({ stubs }, { loading: true });
+ createComponent({ loading: true });
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not show loading icon', () => {
- createComponent({ stubs }, { loading: false });
+ createComponent({ loading: false });
expect(findLoadingIcon().exists()).toBe(false);
});
@@ -85,22 +86,17 @@ describe('Pipeline Trigger Author Token', () => {
beforeEach(() => {});
it('renders all trigger authors', () => {
- createComponent({ stubs }, { users, loading: false });
+ createComponent({ users, loading: false });
// should have length of all users plus the static 'Any' option
expect(findAllFilteredSearchSuggestions()).toHaveLength(users.length + 1);
});
it('renders only the trigger author searched for', () => {
- createComponent(
- { stubs },
- {
- users: [
- { name: 'Arnold', username: 'admin', state: 'active', avatar_url: 'avatar-link' },
- ],
- loading: false,
- },
- );
+ createComponent({
+ users: [{ name: 'Arnold', username: 'admin', state: 'active', avatar_url: 'avatar-link' }],
+ loading: false,
+ });
expect(findAllFilteredSearchSuggestions()).toHaveLength(2);
});
diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js
index 4da82152818..7834456f7c4 100644
--- a/spec/frontend/profile/account/components/delete_account_modal_spec.js
+++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js
@@ -1,21 +1,49 @@
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { merge } from 'lodash';
+import { mount } from '@vue/test-utils';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
+const GlModalStub = {
+ name: 'gl-modal-stub',
+ template: `
+ <div>
+ <slot></slot>
+ </div>
+ `,
+};
+
describe('DeleteAccountModal component', () => {
const actionUrl = `${TEST_HOST}/delete/user`;
const username = 'hasnoname';
- let Component;
+ let wrapper;
let vm;
- beforeEach(() => {
- Component = Vue.extend(deleteAccountModal);
- });
+ const createWrapper = (options = {}) => {
+ wrapper = mount(
+ deleteAccountModal,
+ merge(
+ {},
+ {
+ propsData: {
+ actionUrl,
+ username,
+ },
+ stubs: {
+ GlModal: GlModalStub,
+ },
+ },
+ options,
+ ),
+ );
+ vm = wrapper.vm;
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
+ vm = null;
});
const findElements = () => {
@@ -23,16 +51,16 @@ describe('DeleteAccountModal component', () => {
return {
form: vm.$refs.form,
input: vm.$el.querySelector(`[name="${confirmation}"]`),
- submitButton: vm.$el.querySelector('.btn-danger'),
};
};
+ const findModal = () => wrapper.find(GlModalStub);
describe('with password confirmation', () => {
beforeEach(done => {
- vm = mountComponent(Component, {
- actionUrl,
- confirmWithPassword: true,
- username,
+ createWrapper({
+ propsData: {
+ confirmWithPassword: true,
+ },
});
vm.isOpen = true;
@@ -43,7 +71,7 @@ describe('DeleteAccountModal component', () => {
});
it('does not accept empty password', done => {
- const { form, input, submitButton } = findElements();
+ const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = '';
input.dispatchEvent(new Event('input'));
@@ -51,8 +79,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
- expect(submitButton).toHaveAttr('disabled', 'disabled');
- submitButton.click();
+ expect(findModal().attributes('ok-disabled')).toBe('true');
+ findModal().vm.$emit('primary');
expect(form.submit).not.toHaveBeenCalled();
})
@@ -61,7 +89,7 @@ describe('DeleteAccountModal component', () => {
});
it('submits form with password', done => {
- const { form, input, submitButton } = findElements();
+ const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'anything';
input.dispatchEvent(new Event('input'));
@@ -69,8 +97,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
- expect(submitButton).not.toHaveAttr('disabled', 'disabled');
- submitButton.click();
+ expect(findModal().attributes('ok-disabled')).toBeUndefined();
+ findModal().vm.$emit('primary');
expect(form.submit).toHaveBeenCalled();
})
@@ -81,10 +109,10 @@ describe('DeleteAccountModal component', () => {
describe('with username confirmation', () => {
beforeEach(done => {
- vm = mountComponent(Component, {
- actionUrl,
- confirmWithPassword: false,
- username,
+ createWrapper({
+ propsData: {
+ confirmWithPassword: false,
+ },
});
vm.isOpen = true;
@@ -95,7 +123,7 @@ describe('DeleteAccountModal component', () => {
});
it('does not accept wrong username', done => {
- const { form, input, submitButton } = findElements();
+ const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'this is wrong';
input.dispatchEvent(new Event('input'));
@@ -103,8 +131,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
- expect(submitButton).toHaveAttr('disabled', 'disabled');
- submitButton.click();
+ expect(findModal().attributes('ok-disabled')).toBe('true');
+ findModal().vm.$emit('primary');
expect(form.submit).not.toHaveBeenCalled();
})
@@ -113,7 +141,7 @@ describe('DeleteAccountModal component', () => {
});
it('submits form with correct username', done => {
- const { form, input, submitButton } = findElements();
+ const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = username;
input.dispatchEvent(new Event('input'));
@@ -121,8 +149,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
- expect(submitButton).not.toHaveAttr('disabled', 'disabled');
- submitButton.click();
+ expect(findModal().attributes('ok-disabled')).toBeUndefined();
+ findModal().vm.$emit('primary');
expect(form.submit).toHaveBeenCalled();
})
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index d6fac6f5f79..68c285a4097 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -1,11 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import {
- GlNewDropdown,
- GlNewDropdownHeader,
- GlSearchBoxByType,
- GlNewDropdownItem,
-} from '@gitlab/ui';
+import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
@@ -63,10 +58,10 @@ describe('Author Select', () => {
});
const findDropdownContainer = () => wrapper.find({ ref: 'dropdownContainer' });
- const findDropdown = () => wrapper.find(GlNewDropdown);
- const findDropdownHeader = () => wrapper.find(GlNewDropdownHeader);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownHeader = () => wrapper.find(GlDropdownSectionHeader);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
- const findDropdownItems = () => wrapper.findAll(GlNewDropdownItem);
+ const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
describe('user is searching via "filter by commit message"', () => {
it('disables dropdown container', () => {
@@ -133,11 +128,7 @@ describe('Author Select', () => {
const authorName = 'lorem';
findSearchBox().vm.$emit('input', authorName);
- expect(store.actions.fetchAuthors).toHaveBeenCalledWith(
- expect.anything(),
- authorName,
- undefined,
- );
+ expect(store.actions.fetchAuthors).toHaveBeenCalledWith(expect.anything(), authorName);
});
});
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index 44220bdef64..455467e7b29 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -51,12 +51,12 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
variant="danger"
>
<gl-sprintf-stub
- message="Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
+ message="Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
/>
</gl-alert-stub>
<p>
- This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.
+ This action cannot be undone. You will lose the project's repository and all content: issues, merge requests, etc.
</p>
<p
@@ -66,7 +66,9 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
</p>
<p>
- <code>
+ <code
+ class="gl-white-space-pre-wrap"
+ >
foo
</code>
</p>
diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
index a43acc8c002..692b8f6cf52 100644
--- a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
@@ -55,7 +55,9 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
</p>
<p>
- <code>
+ <code
+ class="gl-white-space-pre-wrap"
+ >
foo
</code>
</p>
diff --git a/spec/frontend/projects/settings/access_dropdown_spec.js b/spec/frontend/projects/settings/access_dropdown_spec.js
index 6d323b0408b..3b375c5610f 100644
--- a/spec/frontend/projects/settings/access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/access_dropdown_spec.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import '~/gl_dropdown';
import AccessDropdown from '~/projects/settings/access_dropdown';
import { LEVEL_TYPES } from '~/projects/settings/constants';
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 4c873bdfd60..0f3b699f6b2 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -195,7 +195,7 @@ describe('ServiceDeskRoot', () => {
.$nextTick()
.then(waitForPromises)
.then(() => {
- expect(wrapper.html()).toContain('Template was successfully saved.');
+ expect(wrapper.html()).toContain('Changes were successfully made.');
});
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 7fe310aa400..cb46751f66a 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -26,16 +26,16 @@ describe('ServiceDeskSetting', () => {
});
it('should see activation checkbox', () => {
- expect(wrapper.contains('#service-desk-checkbox')).toBe(true);
+ expect(wrapper.find('#service-desk-checkbox').exists()).toBe(true);
});
it('should see main panel with the email info', () => {
- expect(wrapper.contains('#incoming-email-describer')).toBe(true);
+ expect(wrapper.find('#incoming-email-describer').exists()).toBe(true);
});
it('should see loading spinner and not the incoming email', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.contains('.incoming-email')).toBe(false);
+ expect(wrapper.find('.incoming-email').exists()).toBe(false);
});
});
});
@@ -78,7 +78,7 @@ describe('ServiceDeskSetting', () => {
});
it('renders a copy to clipboard button', () => {
- expect(wrapper.contains('.qa-clipboard-button')).toBe(true);
+ expect(wrapper.find('.qa-clipboard-button').exists()).toBe(true);
expect(wrapper.find('.qa-clipboard-button').element.dataset.clipboardText).toBe(
incomingEmail,
);
@@ -93,7 +93,7 @@ describe('ServiceDeskSetting', () => {
},
});
- expect(wrapper.contains('#service-desk-template-select')).toBe(true);
+ expect(wrapper.find('#service-desk-template-select').exists()).toBe(true);
});
it('renders a dropdown with a default value of ""', () => {
@@ -163,7 +163,7 @@ describe('ServiceDeskSetting', () => {
},
});
- expect(wrapper.find('button.btn-success').text()).toContain('Save template');
+ expect(wrapper.find('button.btn-success').text()).toContain('Save changes');
});
it('emits a save event with the chosen template when the save button is clicked', () => {
@@ -202,15 +202,15 @@ describe('ServiceDeskSetting', () => {
});
it('does not render email panel', () => {
- expect(wrapper.contains('#incoming-email-describer')).toBe(false);
+ expect(wrapper.find('#incoming-email-describer').exists()).toBe(false);
});
it('does not render template dropdown', () => {
- expect(wrapper.contains('#service-desk-template-select')).toBe(false);
+ expect(wrapper.find('#service-desk-template-select').exists()).toBe(false);
});
it('does not render template save button', () => {
- expect(wrapper.contains('button.btn-success')).toBe(false);
+ expect(wrapper.find('button.btn-success').exists()).toBe(false);
});
it('emits an event to turn on Service Desk when the toggle is clicked', () => {
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 1556f5b19dc..00b1d5cfbe2 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -2,9 +2,10 @@ import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { GlLoadingIcon, GlSearchBoxByType, GlNewDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { sprintf } from '~/locale';
+import { ENTER_KEY } from '~/lib/utils/keys';
import RefSelector from '~/ref/components/ref_selector.vue';
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
import createStore from '~/ref/stores/';
@@ -83,16 +84,18 @@ describe('Ref selector component', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findSearchBox = () => wrapper.find(GlSearchBoxByType);
+
const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
- const findBranchDropdownItems = () => findBranchesSection().findAll(GlNewDropdownItem);
+ const findBranchDropdownItems = () => findBranchesSection().findAll(GlDropdownItem);
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
- const findTagDropdownItems = () => findTagsSection().findAll(GlNewDropdownItem);
+ const findTagDropdownItems = () => findTagsSection().findAll(GlDropdownItem);
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
- const findCommitDropdownItems = () => findCommitsSection().findAll(GlNewDropdownItem);
+ const findCommitDropdownItems = () => findCommitsSection().findAll(GlDropdownItem);
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
//
@@ -120,7 +123,7 @@ describe('Ref selector component', () => {
// Convenience methods
//
const updateQuery = newQuery => {
- wrapper.find(GlSearchBoxByType).vm.$emit('input', newQuery);
+ findSearchBox().vm.$emit('input', newQuery);
};
const selectFirstBranch = () => {
@@ -174,7 +177,7 @@ describe('Ref selector component', () => {
return waitForRequests();
});
- it('adds the provided ID to the GlNewDropdown instance', () => {
+ it('adds the provided ID to the GlDropdown instance', () => {
expect(wrapper.attributes().id).toBe(id);
});
});
@@ -244,6 +247,23 @@ describe('Ref selector component', () => {
});
});
+ describe('when the Enter is pressed', () => {
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests({ andClearMocks: true });
+ });
+
+ it('requeries the endpoints when Enter is pressed', () => {
+ findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ return waitForRequests().then(() => {
+ expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
+ expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
describe('when no results are found', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/registry/explorer/components/delete_button_spec.js
index bb0fe81117a..a79ca77a464 100644
--- a/spec/frontend/registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/registry/explorer/components/delete_button_spec.js
@@ -54,7 +54,6 @@ describe('delete_button', () => {
mountComponent({ disabled: true });
expect(findButton().attributes()).toMatchObject({
'aria-label': 'Foo title',
- category: 'secondary',
icon: 'remove',
title: 'Foo title',
variant: 'danger',
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
index cb31efa428f..fc93e9094c9 100644
--- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import component from '~/registry/explorer/components/details_page/details_header.vue';
import { DETAILS_PAGE_TITLE } from '~/registry/explorer/constants';
@@ -11,6 +12,7 @@ describe('Details Header', () => {
propsData,
stubs: {
GlSprintf,
+ TitleArea,
},
});
};
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index a21facefc97..ef22979ca7d 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -6,7 +6,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
-import DetailsRow from '~/registry/shared/components/details_row.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
index 1f560753476..401202026bb 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
@@ -115,7 +115,6 @@ describe('Tags List', () => {
// The list has only two tags and for some reasons .at(-1) does not work
expect(rows.at(1).attributes()).toMatchObject({
- last: 'true',
isdesktop: 'true',
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
index b0291de5f3c..b4471ab8122 100644
--- a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
@@ -1,10 +1,10 @@
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
-import { GlDeprecatedDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
+import { GlDeprecatedDropdown } from '@gitlab/ui';
import Tracking from '~/tracking';
import * as getters from '~/registry/explorer/stores/getters';
import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import {
QUICK_START,
@@ -24,7 +24,7 @@ describe('cli_commands', () => {
let store;
const findDropdownButton = () => wrapper.find(GlDeprecatedDropdown);
- const findFormGroups = () => wrapper.findAll(GlFormGroup);
+ const findCodeInstruction = () => wrapper.findAll(CodeInstruction);
const mountComponent = () => {
store = new Vuex.Store({
@@ -67,54 +67,29 @@ describe('cli_commands', () => {
});
describe.each`
- index | id | labelText | titleText | getter | trackedEvent
- ${0} | ${'docker-login-btn'} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${'dockerLoginCommand'} | ${'click_copy_login'}
- ${1} | ${'docker-build-btn'} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${'dockerBuildCommand'} | ${'click_copy_build'}
- ${2} | ${'docker-push-btn'} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${'dockerPushCommand'} | ${'click_copy_push'}
- `('form group at $index', ({ index, id, labelText, titleText, getter, trackedEvent }) => {
- let formGroup;
-
- const findFormInputGroup = parent => parent.find(GlFormInputGroup);
- const findClipboardButton = parent => parent.find(ClipboardButton);
+ index | labelText | titleText | getter | trackedEvent
+ ${0} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${'dockerLoginCommand'} | ${'click_copy_login'}
+ ${1} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${'dockerBuildCommand'} | ${'click_copy_build'}
+ ${2} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${'dockerPushCommand'} | ${'click_copy_push'}
+ `('code instructions at $index', ({ index, labelText, titleText, getter, trackedEvent }) => {
+ let codeInstruction;
beforeEach(() => {
- formGroup = findFormGroups().at(index);
+ codeInstruction = findCodeInstruction().at(index);
});
it('exists', () => {
- expect(formGroup.exists()).toBe(true);
- });
-
- it(`has a label ${labelText}`, () => {
- expect(formGroup.text()).toBe(labelText);
- });
-
- it(`contains a form input group with ${id} id and with value equal to ${getter} getter`, () => {
- const formInputGroup = findFormInputGroup(formGroup);
- expect(formInputGroup.exists()).toBe(true);
- expect(formInputGroup.attributes('id')).toBe(id);
- expect(formInputGroup.props('value')).toBe(store.getters[getter]);
- });
-
- it(`contains a clipboard button with title of ${titleText} and text equal to ${getter} getter`, () => {
- const clipBoardButton = findClipboardButton(formGroup);
- expect(clipBoardButton.exists()).toBe(true);
- expect(clipBoardButton.props('title')).toBe(titleText);
- expect(clipBoardButton.props('text')).toBe(store.getters[getter]);
+ expect(codeInstruction.exists()).toBe(true);
});
- it('clipboard button tracks click event', () => {
- const clipBoardButton = findClipboardButton(formGroup);
- clipBoardButton.trigger('click');
- /* This expect to be called with first argument undefined so that
- * the function internally can default to document.body.dataset.page
- * https://docs.gitlab.com/ee/telemetry/frontend.html#tracking-within-vue-components
- */
- expect(Tracking.event).toHaveBeenCalledWith(
- undefined,
- trackedEvent,
- expect.objectContaining({ label: 'quickstart_dropdown' }),
- );
+ it(`has the correct props`, () => {
+ expect(codeInstruction.props()).toMatchObject({
+ label: labelText,
+ instruction: store.getters[getter],
+ copyText: titleText,
+ trackingAction: trackedEvent,
+ trackingLabel: 'quickstart_dropdown',
+ });
});
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
index aaeaaf00748..c5b4b3fa5d8 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
@@ -3,7 +3,7 @@ import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
-import ListItem from '~/registry/explorer/components/list_item.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
diff --git a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
index 7484fccbea7..7a27f8fa431 100644
--- a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
@@ -1,12 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { GlSprintf, GlLink } from '@gitlab/ui';
import Component from '~/registry/explorer/components/list_page/registry_header.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
EXPIRATION_POLICY_DISABLED_TEXT,
- EXPIRATION_POLICY_WILL_RUN_IN,
} from '~/registry/explorer/constants';
jest.mock('~/lib/utils/datetime_utility', () => ({
@@ -17,12 +17,10 @@ jest.mock('~/lib/utils/datetime_utility', () => ({
describe('registry_header', () => {
let wrapper;
- const findHeader = () => wrapper.find('[data-testid="header"]');
- const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findTitleArea = () => wrapper.find(TitleArea);
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findInfoArea = () => wrapper.find('[data-testid="info-area"]');
const findIntroText = () => wrapper.find('[data-testid="default-intro"]');
- const findSubHeader = () => wrapper.find('[data-testid="subheader"]');
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
const findDisabledExpirationPolicyMessage = () =>
@@ -32,10 +30,12 @@ describe('registry_header', () => {
wrapper = shallowMount(Component, {
stubs: {
GlSprintf,
+ TitleArea,
},
propsData,
slots,
});
+ return wrapper.vm.$nextTick();
};
afterEach(() => {
@@ -44,90 +44,80 @@ describe('registry_header', () => {
});
describe('header', () => {
- it('exists', () => {
+ it('has a title', () => {
mountComponent();
- expect(findHeader().exists()).toBe(true);
- });
- it('contains the title of the page', () => {
- mountComponent();
- const title = findTitle();
- expect(title.exists()).toBe(true);
- expect(title.text()).toBe(CONTAINER_REGISTRY_TITLE);
+ expect(findTitleArea().props('title')).toBe(CONTAINER_REGISTRY_TITLE);
});
it('has a commands slot', () => {
- mountComponent(null, { commands: 'baz' });
+ mountComponent(null, { commands: '<div data-testid="commands-slot">baz</div>' });
+
expect(findCommandsSlot().text()).toBe('baz');
});
- });
- describe('subheader', () => {
- describe('when there are no images', () => {
- it('is hidden ', () => {
- mountComponent();
- expect(findSubHeader().exists()).toBe(false);
- });
- });
+ describe('sub header parts', () => {
+ describe('images count', () => {
+ it('exists', async () => {
+ await mountComponent({ imagesCount: 1 });
- describe('when there are images', () => {
- it('is visible', () => {
- mountComponent({ imagesCount: 1 });
- expect(findSubHeader().exists()).toBe(true);
- });
+ expect(findImagesCountSubHeader().exists()).toBe(true);
+ });
+
+ it('when there is one image', async () => {
+ await mountComponent({ imagesCount: 1 });
- describe('sub header parts', () => {
- describe('images count', () => {
- it('exists', () => {
- mountComponent({ imagesCount: 1 });
- expect(findImagesCountSubHeader().exists()).toBe(true);
+ expect(findImagesCountSubHeader().props()).toMatchObject({
+ text: '1 Image repository',
+ icon: 'container-image',
});
+ });
+
+ it('when there is more than one image', async () => {
+ await mountComponent({ imagesCount: 3 });
+
+ expect(findImagesCountSubHeader().props('text')).toBe('3 Image repositories');
+ });
+ });
- it('when there is one image', () => {
- mountComponent({ imagesCount: 1 });
- expect(findImagesCountSubHeader().text()).toMatchInterpolatedText('1 Image repository');
+ describe('expiration policy', () => {
+ it('when is disabled', async () => {
+ await mountComponent({
+ expirationPolicy: { enabled: false },
+ expirationPolicyHelpPagePath: 'foo',
+ imagesCount: 1,
});
- it('when there is more than one image', () => {
- mountComponent({ imagesCount: 3 });
- expect(findImagesCountSubHeader().text()).toMatchInterpolatedText(
- '3 Image repositories',
- );
+ const text = findExpirationPolicySubHeader();
+ expect(text.exists()).toBe(true);
+ expect(text.props()).toMatchObject({
+ text: EXPIRATION_POLICY_DISABLED_TEXT,
+ icon: 'expire',
+ size: 'xl',
});
});
- describe('expiration policy', () => {
- it('when is disabled', () => {
- mountComponent({
- expirationPolicy: { enabled: false },
- expirationPolicyHelpPagePath: 'foo',
- imagesCount: 1,
- });
- const text = findExpirationPolicySubHeader();
- expect(text.exists()).toBe(true);
- expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_DISABLED_TEXT);
+ it('when is enabled', async () => {
+ await mountComponent({
+ expirationPolicy: { enabled: true },
+ expirationPolicyHelpPagePath: 'foo',
+ imagesCount: 1,
});
- it('when is enabled', () => {
- mountComponent({
- expirationPolicy: { enabled: true },
- expirationPolicyHelpPagePath: 'foo',
- imagesCount: 1,
- });
- const text = findExpirationPolicySubHeader();
- expect(text.exists()).toBe(true);
- expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_WILL_RUN_IN);
- });
- it('when the expiration policy is completely disabled', () => {
- mountComponent({
- expirationPolicy: { enabled: true },
- expirationPolicyHelpPagePath: 'foo',
- imagesCount: 1,
- hideExpirationPolicyData: true,
- });
- const text = findExpirationPolicySubHeader();
- expect(text.exists()).toBe(false);
+ const text = findExpirationPolicySubHeader();
+ expect(text.exists()).toBe(true);
+ expect(text.props('text')).toBe('Expiration policy will run in ');
+ });
+ it('when the expiration policy is completely disabled', async () => {
+ await mountComponent({
+ expirationPolicy: { enabled: true },
+ expirationPolicyHelpPagePath: 'foo',
+ imagesCount: 1,
+ hideExpirationPolicyData: true,
});
+
+ const text = findExpirationPolicySubHeader();
+ expect(text.exists()).toBe(false);
});
});
});
@@ -136,12 +126,13 @@ describe('registry_header', () => {
describe('info area', () => {
it('exists', () => {
mountComponent();
+
expect(findInfoArea().exists()).toBe(true);
});
describe('default message', () => {
beforeEach(() => {
- mountComponent({ helpPagePath: 'bar' });
+ return mountComponent({ helpPagePath: 'bar' });
});
it('exists', () => {
@@ -165,6 +156,7 @@ describe('registry_header', () => {
describe('when there are no images', () => {
it('is hidden', () => {
mountComponent();
+
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
@@ -172,7 +164,7 @@ describe('registry_header', () => {
describe('when there are images', () => {
describe('when expiration policy is disabled', () => {
beforeEach(() => {
- mountComponent({
+ return mountComponent({
expirationPolicy: { enabled: false },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
@@ -202,6 +194,7 @@ describe('registry_header', () => {
expirationPolicy: { enabled: true },
imagesCount: 1,
});
+
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
@@ -212,6 +205,7 @@ describe('registry_header', () => {
imagesCount: 1,
hideExpirationPolicyData: true,
});
+
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
diff --git a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js b/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
index f04585a6ff4..b906e44a4f7 100644
--- a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
@@ -117,7 +117,7 @@ describe('Registry Breadcrumb', () => {
});
it('has the same tag as the last children of the crumbs', () => {
- expect(findLastCrumb().is(lastChildren.tagName)).toBe(true);
+ expect(findLastCrumb().element.tagName).toBe(lastChildren.tagName.toUpperCase());
});
it('has the same classes as the last children of the crumbs', () => {
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index b4e46fda2c4..b24422adb03 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -8,6 +8,7 @@ import GroupEmptyState from '~/registry/explorer/components/list_page/group_empt
import ProjectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
import RegistryHeader from '~/registry/explorer/components/list_page/registry_header.vue';
import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { createStore } from '~/registry/explorer/stores/';
import {
SET_MAIN_LOADING,
@@ -54,6 +55,7 @@ describe('List Page', () => {
GlEmptyState,
GlSprintf,
RegistryHeader,
+ TitleArea,
},
mocks: {
$toast,
diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/registry/explorer/stubs.js
index 8f95fce2867..b6c0ee67757 100644
--- a/spec/frontend/registry/explorer/stubs.js
+++ b/spec/frontend/registry/explorer/stubs.js
@@ -1,5 +1,5 @@
import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
-import RealListItem from '~/registry/explorer/components/list_item.vue';
+import RealListItem from '~/vue_shared/components/registry/list_item.vue';
export const GlModal = {
template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
diff --git a/spec/frontend/registry/shared/mocks.js b/spec/frontend/registry/shared/mocks.js
index e33d06e7499..fdef38b6f10 100644
--- a/spec/frontend/registry/shared/mocks.js
+++ b/spec/frontend/registry/shared/mocks.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const $toast = {
show: jest.fn(),
};
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
new file mode 100644
index 00000000000..f56e296d106
--- /dev/null
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`releases/util.js convertGraphQLResponse matches snapshot 1`] = `
+Object {
+ "data": Array [
+ Object {
+ "_links": Object {
+ "editUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit",
+ "issuesUrl": null,
+ "mergeRequestsUrl": null,
+ "self": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10",
+ "selfUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10",
+ },
+ "assets": Object {
+ "count": 7,
+ "links": Array [
+ Object {
+ "directAssetUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook",
+ "external": true,
+ "id": "gid://gitlab/Releases::Link/69",
+ "linkType": "other",
+ "name": "An example link",
+ "url": "https://example.com/link",
+ },
+ Object {
+ "directAssetUrl": "https://example.com/package",
+ "external": true,
+ "id": "gid://gitlab/Releases::Link/68",
+ "linkType": "package",
+ "name": "An example package link",
+ "url": "https://example.com/package",
+ },
+ Object {
+ "directAssetUrl": "https://example.com/image",
+ "external": true,
+ "id": "gid://gitlab/Releases::Link/67",
+ "linkType": "image",
+ "name": "An example image",
+ "url": "https://example.com/image",
+ },
+ ],
+ "sources": Array [
+ Object {
+ "format": "zip",
+ "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip",
+ },
+ Object {
+ "format": "tar.gz",
+ "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz",
+ },
+ Object {
+ "format": "tar.bz2",
+ "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2",
+ },
+ Object {
+ "format": "tar",
+ "url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar",
+ },
+ ],
+ },
+ "author": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "username": "root",
+ "webUrl": "http://0.0.0.0:3000/root",
+ },
+ "commit": Object {
+ "shortId": "92e7ea2e",
+ "title": "Testing a change.",
+ },
+ "commitPath": "http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7",
+ "descriptionHtml": "<p data-sourcepos=\\"1:1-1:24\\" dir=\\"auto\\">This is version <strong>1.0</strong>!</p>",
+ "evidences": Array [
+ Object {
+ "collectedAt": "2020-08-21T20:15:19Z",
+ "filepath": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json",
+ "sha": "22bde8e8b93d870a29ddc339287a1fbb598f45d1396d",
+ },
+ ],
+ "milestones": Array [
+ Object {
+ "description": "",
+ "id": "gid://gitlab/Milestone/60",
+ "issueStats": Object {
+ "closed": 0,
+ "total": 0,
+ },
+ "stats": undefined,
+ "title": "12.4",
+ "webPath": undefined,
+ "webUrl": "/root/release-test/-/milestones/2",
+ },
+ Object {
+ "description": "Milestone 12.3",
+ "id": "gid://gitlab/Milestone/59",
+ "issueStats": Object {
+ "closed": 1,
+ "total": 2,
+ },
+ "stats": undefined,
+ "title": "12.3",
+ "webPath": undefined,
+ "webUrl": "/root/release-test/-/milestones/1",
+ },
+ ],
+ "name": "Release 1.0",
+ "releasedAt": "2020-08-21T20:15:18Z",
+ "tagName": "v5.10",
+ "tagPath": "/root/release-test/-/tags/v5.10",
+ "upcomingRelease": false,
+ },
+ ],
+}
+`;
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 8eafe07cb2f..bcb87509cc3 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -1,12 +1,11 @@
import { range as rge } from 'lodash';
-import Vue from 'vue';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import app from '~/releases/components/app_index.vue';
+import ReleasesApp from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
-import listModule from '~/releases/stores/modules/list';
+import createListModule from '~/releases/stores/modules/list';
import api from '~/api';
-import { resetStore } from '../stores/modules/list/helpers';
import {
pageInfoHeadersWithoutPagination,
pageInfoHeadersWithPagination,
@@ -14,30 +13,67 @@ import {
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
describe('Releases App ', () => {
- const Component = Vue.extend(app);
- let store;
- let vm;
- let releasesPagination;
+ let wrapper;
+ let fetchReleaseSpy;
+
+ const releasesPagination = rge(21).map(index => ({
+ ...convertObjectPropsToCamelCase(release, { deep: true }),
+ tagName: `${index}.00`,
+ }));
- const props = {
+ const defaultInitialState = {
projectId: 'gitlab-ce',
+ projectPath: 'gitlab-org/gitlab-ce',
documentationPath: 'help/releases',
illustrationPath: 'illustration/path',
};
- beforeEach(() => {
- store = createStore({ modules: { list: listModule } });
- releasesPagination = rge(21).map(index => ({
- ...convertObjectPropsToCamelCase(release, { deep: true }),
- tagName: `${index}.00`,
- }));
- });
+ const createComponent = (stateUpdates = {}) => {
+ const listModule = createListModule({
+ ...defaultInitialState,
+ ...stateUpdates,
+ });
+
+ fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases');
+
+ const store = createStore({
+ modules: { list: listModule },
+ featureFlags: {
+ graphqlReleaseData: true,
+ graphqlReleasesPage: false,
+ graphqlMilestoneStats: true,
+ },
+ });
+
+ wrapper = shallowMount(ReleasesApp, {
+ store,
+ localVue,
+ });
+ };
afterEach(() => {
- resetStore(store);
- vm.$destroy();
+ wrapper.destroy();
+ });
+
+ describe('on startup', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(api, 'releases')
+ .mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
+
+ createComponent();
+ });
+
+ it('calls fetchRelease with the page parameter', () => {
+ expect(fetchReleaseSpy).toHaveBeenCalledTimes(1);
+ expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), { page: null });
+ });
});
describe('while loading', () => {
@@ -47,16 +83,15 @@ describe('Releases App ', () => {
// Need to defer the return value here to the next stack,
// otherwise the loading state disappears before our test even starts.
.mockImplementation(() => waitForPromises().then(() => ({ data: [], headers: {} })));
- vm = mountComponentWithStore(Component, { props, store });
+
+ createComponent();
});
it('renders loading icon', () => {
- expect(vm.$el.querySelector('.js-loading')).not.toBeNull();
- expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
- expect(vm.$el.querySelector('.js-success-state')).toBeNull();
- expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
-
- return waitForPromises();
+ expect(wrapper.contains('.js-loading')).toBe(true);
+ expect(wrapper.contains('.js-empty-state')).toBe(false);
+ expect(wrapper.contains('.js-success-state')).toBe(false);
+ expect(wrapper.contains(TablePagination)).toBe(false);
});
});
@@ -65,14 +100,15 @@ describe('Releases App ', () => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
- vm = mountComponentWithStore(Component, { props, store });
+
+ createComponent();
});
it('renders success state', () => {
- expect(vm.$el.querySelector('.js-loading')).toBeNull();
- expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
- expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
- expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
+ expect(wrapper.contains('.js-loading')).toBe(false);
+ expect(wrapper.contains('.js-empty-state')).toBe(false);
+ expect(wrapper.contains('.js-success-state')).toBe(true);
+ expect(wrapper.contains(TablePagination)).toBe(true);
});
});
@@ -81,69 +117,60 @@ describe('Releases App ', () => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releasesPagination, headers: pageInfoHeadersWithPagination });
- vm = mountComponentWithStore(Component, { props, store });
+
+ createComponent();
});
it('renders success state', () => {
- expect(vm.$el.querySelector('.js-loading')).toBeNull();
- expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
- expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
- expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull();
+ expect(wrapper.contains('.js-loading')).toBe(false);
+ expect(wrapper.contains('.js-empty-state')).toBe(false);
+ expect(wrapper.contains('.js-success-state')).toBe(true);
+ expect(wrapper.contains(TablePagination)).toBe(true);
});
});
describe('with empty request', () => {
beforeEach(() => {
jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} });
- vm = mountComponentWithStore(Component, { props, store });
+
+ createComponent();
});
it('renders empty state', () => {
- expect(vm.$el.querySelector('.js-loading')).toBeNull();
- expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
- expect(vm.$el.querySelector('.js-success-state')).toBeNull();
- expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
+ expect(wrapper.contains('.js-loading')).toBe(false);
+ expect(wrapper.contains('.js-empty-state')).toBe(true);
+ expect(wrapper.contains('.js-success-state')).toBe(false);
});
});
describe('"New release" button', () => {
- const findNewReleaseButton = () => vm.$el.querySelector('.js-new-release-btn');
+ const findNewReleaseButton = () => wrapper.find('.js-new-release-btn');
beforeEach(() => {
jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} });
});
- const factory = additionalProps => {
- vm = mountComponentWithStore(Component, {
- props: {
- ...props,
- ...additionalProps,
- },
- store,
- });
- };
-
describe('when the user is allowed to create a new Release', () => {
const newReleasePath = 'path/to/new/release';
beforeEach(() => {
- factory({ newReleasePath });
+ createComponent({ ...defaultInitialState, newReleasePath });
});
it('renders the "New release" button', () => {
- expect(findNewReleaseButton()).not.toBeNull();
+ expect(findNewReleaseButton().exists()).toBe(true);
});
it('renders the "New release" button with the correct href', () => {
- expect(findNewReleaseButton().getAttribute('href')).toBe(newReleasePath);
+ expect(findNewReleaseButton().attributes('href')).toBe(newReleasePath);
});
});
describe('when the user is not allowed to create a new Release', () => {
- beforeEach(() => factory());
+ beforeEach(() => createComponent());
it('does not render the "New release" button', () => {
- expect(findNewReleaseButton()).toBeNull();
+ expect(findNewReleaseButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index e757fe98661..502a1053663 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -1,6 +1,6 @@
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import { release as originalRelease } from '../mock_data';
import ReleaseBlock from '~/releases/components/release_block.vue';
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 727d593d851..582c0b32716 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -115,14 +115,10 @@ describe('Release edit component', () => {
const expectStoreMethodToBeCalled = () => {
expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1);
- expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(
- expect.anything(),
- {
- linkIdToUpdate,
- newUrl,
- },
- undefined,
- );
+ expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(expect.anything(), {
+ linkIdToUpdate,
+ newUrl,
+ });
};
it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => {
@@ -177,14 +173,10 @@ describe('Release edit component', () => {
const expectStoreMethodToBeCalled = () => {
expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1);
- expect(actions.updateAssetLinkName).toHaveBeenCalledWith(
- expect.anything(),
- {
- linkIdToUpdate,
- newName,
- },
- undefined,
- );
+ expect(actions.updateAssetLinkName).toHaveBeenCalledWith(expect.anything(), {
+ linkIdToUpdate,
+ newName,
+ });
};
it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
@@ -225,14 +217,10 @@ describe('Release edit component', () => {
wrapper.find({ ref: 'typeSelect' }).vm.$emit('change', newType);
expect(actions.updateAssetLinkType).toHaveBeenCalledTimes(1);
- expect(actions.updateAssetLinkType).toHaveBeenCalledWith(
- expect.anything(),
- {
- linkIdToUpdate,
- newType,
- },
- undefined,
- );
+ expect(actions.updateAssetLinkType).toHaveBeenCalledWith(expect.anything(), {
+ linkIdToUpdate,
+ newType,
+ });
});
it('selects the default asset type if no type was provided by the backend', () => {
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index 5e84290716c..3453ecbf8ab 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -128,7 +128,7 @@ describe('Release block assets', () => {
describe('external vs internal links', () => {
const containsExternalSourceIndicator = () =>
- wrapper.contains('[data-testid="external-link-indicator"]');
+ wrapper.find('[data-testid="external-link-indicator"]').exists();
describe('when a link is external', () => {
beforeEach(() => {
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index c066bfbf020..bde01cc0e00 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,9 +1,8 @@
import { mount } from '@vue/test-utils';
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { cloneDeep } from 'lodash';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -56,7 +55,7 @@ describe('Release block footer', () => {
beforeEach(() => factory());
it('renders the commit icon', () => {
- const commitIcon = commitInfoSection().find(Icon);
+ const commitIcon = commitInfoSection().find(GlIcon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('commit');
@@ -71,7 +70,7 @@ describe('Release block footer', () => {
});
it('renders the tag icon', () => {
- const commitIcon = tagInfoSection().find(Icon);
+ const commitIcon = tagInfoSection().find(GlIcon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('tag');
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 19119d99f3c..a7f1388664b 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import { mount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release as originalRelease } from '../mock_data';
-import Icon from '~/vue_shared/components/icon.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import * as urlUtility from '~/lib/utils/url_utility';
@@ -247,7 +247,7 @@ describe('Release block', () => {
it('renders the milestone icon', () => {
expect(
milestoneListLabel()
- .find(Icon)
+ .find(GlIcon)
.exists(),
).toBe(true);
});
diff --git a/spec/frontend/releases/components/releases_pagination_graphql_spec.js b/spec/frontend/releases/components/releases_pagination_graphql_spec.js
new file mode 100644
index 00000000000..b01a28eb6c3
--- /dev/null
+++ b/spec/frontend/releases/components/releases_pagination_graphql_spec.js
@@ -0,0 +1,175 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import createStore from '~/releases/stores';
+import createListModule from '~/releases/stores/modules/list';
+import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue';
+import { historyPushState } from '~/lib/utils/common_utils';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ historyPushState: jest.fn(),
+}));
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('~/releases/components/releases_pagination_graphql.vue', () => {
+ let wrapper;
+ let listModule;
+
+ const cursors = {
+ startCursor: 'startCursor',
+ endCursor: 'endCursor',
+ };
+
+ const projectPath = 'my/project';
+
+ const createComponent = pageInfo => {
+ listModule = createListModule({ projectPath });
+
+ listModule.state.graphQlPageInfo = pageInfo;
+
+ listModule.actions.fetchReleasesGraphQl = jest.fn();
+
+ wrapper = mount(ReleasesPaginationGraphql, {
+ store: createStore({
+ modules: {
+ list: listModule,
+ },
+ featureFlags: {},
+ }),
+ localVue,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findPrevButton = () => wrapper.find('[data-testid="prevButton"]');
+ const findNextButton = () => wrapper.find('[data-testid="nextButton"]');
+
+ const expectDisabledPrev = () => {
+ expect(findPrevButton().attributes().disabled).toBe('disabled');
+ };
+ const expectEnabledPrev = () => {
+ expect(findPrevButton().attributes().disabled).toBe(undefined);
+ };
+ const expectDisabledNext = () => {
+ expect(findNextButton().attributes().disabled).toBe('disabled');
+ };
+ const expectEnabledNext = () => {
+ expect(findNextButton().attributes().disabled).toBe(undefined);
+ };
+
+ describe('when there is only one page of results', () => {
+ beforeEach(() => {
+ createComponent({
+ hasPreviousPage: false,
+ hasNextPage: false,
+ });
+ });
+
+ it('does not render anything', () => {
+ expect(wrapper.isEmpty()).toBe(true);
+ });
+ });
+
+ describe('when there is a next page, but not a previous page', () => {
+ beforeEach(() => {
+ createComponent({
+ hasPreviousPage: false,
+ hasNextPage: true,
+ });
+ });
+
+ it('renders a disabled "Prev" button', () => {
+ expectDisabledPrev();
+ });
+
+ it('renders an enabled "Next" button', () => {
+ expectEnabledNext();
+ });
+ });
+
+ describe('when there is a previous page, but not a next page', () => {
+ beforeEach(() => {
+ createComponent({
+ hasPreviousPage: true,
+ hasNextPage: false,
+ });
+ });
+
+ it('renders a enabled "Prev" button', () => {
+ expectEnabledPrev();
+ });
+
+ it('renders an disabled "Next" button', () => {
+ expectDisabledNext();
+ });
+ });
+
+ describe('when there is both a previous page and a next page', () => {
+ beforeEach(() => {
+ createComponent({
+ hasPreviousPage: true,
+ hasNextPage: true,
+ });
+ });
+
+ it('renders a enabled "Prev" button', () => {
+ expectEnabledPrev();
+ });
+
+ it('renders an enabled "Next" button', () => {
+ expectEnabledNext();
+ });
+ });
+
+ describe('button behavior', () => {
+ beforeEach(() => {
+ createComponent({
+ hasPreviousPage: true,
+ hasNextPage: true,
+ ...cursors,
+ });
+ });
+
+ describe('next button behavior', () => {
+ beforeEach(() => {
+ findNextButton().trigger('click');
+ });
+
+ it('calls fetchReleasesGraphQl with the correct after cursor', () => {
+ expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([
+ [expect.anything(), { after: cursors.endCursor }],
+ ]);
+ });
+
+ it('calls historyPushState with the new URL', () => {
+ expect(historyPushState.mock.calls).toEqual([
+ [expect.stringContaining(`?after=${cursors.endCursor}`)],
+ ]);
+ });
+ });
+
+ describe('previous button behavior', () => {
+ beforeEach(() => {
+ findPrevButton().trigger('click');
+ });
+
+ it('calls fetchReleasesGraphQl with the correct before cursor', () => {
+ expect(listModule.actions.fetchReleasesGraphQl.mock.calls).toEqual([
+ [expect.anything(), { before: cursors.startCursor }],
+ ]);
+ });
+
+ it('calls historyPushState with the new URL', () => {
+ expect(historyPushState.mock.calls).toEqual([
+ [expect.stringContaining(`?before=${cursors.startCursor}`)],
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/releases_pagination_rest_spec.js b/spec/frontend/releases/components/releases_pagination_rest_spec.js
new file mode 100644
index 00000000000..4fd3e085fc9
--- /dev/null
+++ b/spec/frontend/releases/components/releases_pagination_rest_spec.js
@@ -0,0 +1,72 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { GlPagination } from '@gitlab/ui';
+import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue';
+import createStore from '~/releases/stores';
+import createListModule from '~/releases/stores/modules/list';
+import * as commonUtils from '~/lib/utils/common_utils';
+
+commonUtils.historyPushState = jest.fn();
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('~/releases/components/releases_pagination_rest.vue', () => {
+ let wrapper;
+ let listModule;
+
+ const projectId = 19;
+
+ const createComponent = pageInfo => {
+ listModule = createListModule({ projectId });
+
+ listModule.state.pageInfo = pageInfo;
+
+ listModule.actions.fetchReleasesRest = jest.fn();
+
+ wrapper = mount(ReleasesPaginationRest, {
+ store: createStore({
+ modules: {
+ list: listModule,
+ },
+ featureFlags: {},
+ }),
+ localVue,
+ });
+ };
+
+ const findGlPagination = () => wrapper.find(GlPagination);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when a page number is clicked', () => {
+ const newPage = 2;
+
+ beforeEach(() => {
+ createComponent({
+ perPage: 20,
+ page: 1,
+ total: 40,
+ totalPages: 2,
+ nextPage: 2,
+ });
+
+ findGlPagination().vm.$emit('input', newPage);
+ });
+
+ it('calls fetchReleasesRest with the correct page', () => {
+ expect(listModule.actions.fetchReleasesRest.mock.calls).toEqual([
+ [expect.anything(), { page: newPage }],
+ ]);
+ });
+
+ it('calls historyPushState with the new URL', () => {
+ expect(commonUtils.historyPushState.mock.calls).toEqual([
+ [expect.stringContaining(`?page=${newPage}`)],
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
new file mode 100644
index 00000000000..2466fb53a68
--- /dev/null
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -0,0 +1,52 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import ReleasesPagination from '~/releases/components/releases_pagination.vue';
+import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue';
+import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('~/releases/components/releases_pagination.vue', () => {
+ let wrapper;
+
+ const createComponent = useGraphQLEndpoint => {
+ const store = new Vuex.Store({
+ getters: {
+ useGraphQLEndpoint: () => useGraphQLEndpoint,
+ },
+ });
+
+ wrapper = shallowMount(ReleasesPagination, { store, localVue });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findRestPagination = () => wrapper.find(ReleasesPaginationRest);
+ const findGraphQlPagination = () => wrapper.find(ReleasesPaginationGraphql);
+
+ describe('when one of necessary feature flags is disabled', () => {
+ beforeEach(() => {
+ createComponent(false);
+ });
+
+ it('renders the REST pagination component', () => {
+ expect(findRestPagination().exists()).toBe(true);
+ expect(findGraphQlPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when all the necessary feature flags are enabled', () => {
+ beforeEach(() => {
+ createComponent(true);
+ });
+
+ it('renders the GraphQL pagination component', () => {
+ expect(findGraphQlPagination().exists()).toBe(true);
+ expect(findRestPagination().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
index 0a04f68bd67..70a195556df 100644
--- a/spec/frontend/releases/components/tag_field_exsting_spec.js
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -1,5 +1,6 @@
+import Vuex from 'vuex';
import { GlFormInput } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
@@ -7,6 +8,9 @@ import createDetailModule from '~/releases/stores/modules/detail';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_DOCS_PATH = '/help/test/docs/path';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('releases/components/tag_field_existing', () => {
let store;
let wrapper;
@@ -14,6 +18,7 @@ describe('releases/components/tag_field_existing', () => {
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TagFieldExisting, {
store,
+ localVue,
});
};
diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js
index b97385154bd..58cd69a2f6a 100644
--- a/spec/frontend/releases/mock_data.js
+++ b/spec/frontend/releases/mock_data.js
@@ -222,3 +222,131 @@ export const release2 = {
};
export const releases = [release, release2];
+
+export const graphqlReleasesResponse = {
+ data: {
+ project: {
+ releases: {
+ count: 39,
+ nodes: [
+ {
+ name: 'Release 1.0',
+ tagName: 'v5.10',
+ tagPath: '/root/release-test/-/tags/v5.10',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:24" dir="auto">This is version <strong>1.0</strong>!</p>',
+ releasedAt: '2020-08-21T20:15:18Z',
+ upcomingRelease: false,
+ assets: {
+ count: 7,
+ sources: {
+ nodes: [
+ {
+ format: 'zip',
+ url:
+ 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip',
+ },
+ {
+ format: 'tar.gz',
+ url:
+ 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz',
+ },
+ {
+ format: 'tar.bz2',
+ url:
+ 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2',
+ },
+ {
+ format: 'tar',
+ url:
+ 'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar',
+ },
+ ],
+ },
+ links: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Releases::Link/69',
+ name: 'An example link',
+ url: 'https://example.com/link',
+ directAssetUrl:
+ 'http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook',
+ linkType: 'OTHER',
+ external: true,
+ },
+ {
+ id: 'gid://gitlab/Releases::Link/68',
+ name: 'An example package link',
+ url: 'https://example.com/package',
+ directAssetUrl: 'https://example.com/package',
+ linkType: 'PACKAGE',
+ external: true,
+ },
+ {
+ id: 'gid://gitlab/Releases::Link/67',
+ name: 'An example image',
+ url: 'https://example.com/image',
+ directAssetUrl: 'https://example.com/image',
+ linkType: 'IMAGE',
+ external: true,
+ },
+ ],
+ },
+ },
+ evidences: {
+ nodes: [
+ {
+ filepath:
+ 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json',
+ collectedAt: '2020-08-21T20:15:19Z',
+ sha: '22bde8e8b93d870a29ddc339287a1fbb598f45d1396d',
+ },
+ ],
+ },
+ links: {
+ editUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit',
+ issuesUrl: null,
+ mergeRequestsUrl: null,
+ selfUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10',
+ },
+ commit: {
+ sha: '92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7',
+ webUrl:
+ 'http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7',
+ title: 'Testing a change.',
+ },
+ author: {
+ webUrl: 'http://0.0.0.0:3000/root',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ username: 'root',
+ },
+ milestones: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Milestone/60',
+ title: '12.4',
+ description: '',
+ webPath: '/root/release-test/-/milestones/2',
+ stats: {
+ totalIssuesCount: 0,
+ closedIssuesCount: 0,
+ },
+ },
+ {
+ id: 'gid://gitlab/Milestone/59',
+ title: '12.3',
+ description: 'Milestone 12.3',
+ webPath: '/root/release-test/-/milestones/1',
+ stats: {
+ totalIssuesCount: 2,
+ closedIssuesCount: 1,
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
index 4c3af157684..95e30659d6c 100644
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/list/actions_spec.js
@@ -1,3 +1,4 @@
+import { cloneDeep } from 'lodash';
import testAction from 'helpers/vuex_action_helper';
import {
requestReleases,
@@ -5,21 +6,43 @@ import {
receiveReleasesSuccess,
receiveReleasesError,
} from '~/releases/stores/modules/list/actions';
-import state from '~/releases/stores/modules/list/state';
+import createState from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api';
+import { gqClient, convertGraphQLResponse } from '~/releases/util';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { pageInfoHeadersWithoutPagination, releases as originalReleases } from '../../../mock_data';
+import {
+ pageInfoHeadersWithoutPagination,
+ releases as originalReleases,
+ graphqlReleasesResponse as originalGraphqlReleasesResponse,
+} from '../../../mock_data';
+import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
describe('Releases State actions', () => {
let mockedState;
let pageInfo;
let releases;
+ let graphqlReleasesResponse;
+
+ const projectPath = 'root/test-project';
+ const projectId = 19;
beforeEach(() => {
- mockedState = state();
+ mockedState = {
+ ...createState({
+ projectId,
+ projectPath,
+ }),
+ featureFlags: {
+ graphqlReleaseData: true,
+ graphqlReleasesPage: true,
+ graphqlMilestoneStats: true,
+ },
+ };
+
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
+ graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
});
describe('requestReleases', () => {
@@ -31,15 +54,17 @@ describe('Releases State actions', () => {
describe('fetchReleases', () => {
describe('success', () => {
it('dispatches requestReleases and receiveReleasesSuccess', done => {
- jest.spyOn(api, 'releases').mockImplementation((id, options) => {
- expect(id).toEqual(1);
- expect(options.page).toEqual('1');
- return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
+ jest.spyOn(gqClient, 'query').mockImplementation(({ query, variables }) => {
+ expect(query).toBe(allReleasesQuery);
+ expect(variables).toEqual({
+ fullPath: projectPath,
+ });
+ return Promise.resolve(graphqlReleasesResponse);
});
testAction(
fetchReleases,
- { projectId: 1 },
+ {},
mockedState,
[],
[
@@ -47,31 +72,7 @@ describe('Releases State actions', () => {
type: 'requestReleases',
},
{
- payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
- type: 'receiveReleasesSuccess',
- },
- ],
- done,
- );
- });
-
- it('dispatches requestReleases and receiveReleasesSuccess on page two', done => {
- jest.spyOn(api, 'releases').mockImplementation((_, options) => {
- expect(options.page).toEqual('2');
- return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
- });
-
- testAction(
- fetchReleases,
- { page: '2', projectId: 1 },
- mockedState,
- [],
- [
- {
- type: 'requestReleases',
- },
- {
- payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
+ payload: convertGraphQLResponse(graphqlReleasesResponse),
type: 'receiveReleasesSuccess',
},
],
@@ -82,11 +83,11 @@ describe('Releases State actions', () => {
describe('error', () => {
it('dispatches requestReleases and receiveReleasesError', done => {
- jest.spyOn(api, 'releases').mockReturnValue(Promise.reject());
+ jest.spyOn(gqClient, 'query').mockRejectedValue();
testAction(
fetchReleases,
- { projectId: null },
+ {},
mockedState,
[],
[
@@ -101,6 +102,85 @@ describe('Releases State actions', () => {
);
});
});
+
+ describe('when the graphqlReleaseData feature flag is disabled', () => {
+ beforeEach(() => {
+ mockedState.featureFlags.graphqlReleasesPage = false;
+ });
+
+ describe('success', () => {
+ it('dispatches requestReleases and receiveReleasesSuccess', done => {
+ jest.spyOn(api, 'releases').mockImplementation((id, options) => {
+ expect(id).toBe(projectId);
+ expect(options.page).toBe('1');
+ return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
+ });
+
+ testAction(
+ fetchReleases,
+ {},
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReleases',
+ },
+ {
+ payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
+ type: 'receiveReleasesSuccess',
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches requestReleases and receiveReleasesSuccess on page two', done => {
+ jest.spyOn(api, 'releases').mockImplementation((_, options) => {
+ expect(options.page).toBe('2');
+ return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
+ });
+
+ testAction(
+ fetchReleases,
+ { page: '2' },
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReleases',
+ },
+ {
+ payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
+ type: 'receiveReleasesSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ it('dispatches requestReleases and receiveReleasesError', done => {
+ jest.spyOn(api, 'releases').mockReturnValue(Promise.reject());
+
+ testAction(
+ fetchReleases,
+ {},
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReleases',
+ },
+ {
+ type: 'receiveReleasesError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
});
describe('receiveReleasesSuccess', () => {
diff --git a/spec/frontend/releases/stores/modules/list/helpers.js b/spec/frontend/releases/stores/modules/list/helpers.js
index 435ca36047e..3ca255eaf8c 100644
--- a/spec/frontend/releases/stores/modules/list/helpers.js
+++ b/spec/frontend/releases/stores/modules/list/helpers.js
@@ -1,6 +1,5 @@
import state from '~/releases/stores/modules/list/state';
-// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
store.replaceState(state());
};
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
index 3035b916ff6..27ad05846e7 100644
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/list/mutations_spec.js
@@ -1,4 +1,4 @@
-import state from '~/releases/stores/modules/list/state';
+import createState from '~/releases/stores/modules/list/state';
import mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination } from '~/lib/utils/common_utils';
@@ -9,7 +9,7 @@ describe('Releases Store Mutations', () => {
let pageInfo;
beforeEach(() => {
- stateCopy = state();
+ stateCopy = createState({});
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
});
diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js
index 90aa9c4c7d8..f40e5729188 100644
--- a/spec/frontend/releases/util_spec.js
+++ b/spec/frontend/releases/util_spec.js
@@ -1,4 +1,6 @@
-import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
+import { cloneDeep } from 'lodash';
+import { releaseToApiJson, apiJsonToRelease, convertGraphQLResponse } from '~/releases/util';
+import { graphqlReleasesResponse as originalGraphqlReleasesResponse } from './mock_data';
describe('releases/util.js', () => {
describe('releaseToApiJson', () => {
@@ -100,4 +102,55 @@ describe('releases/util.js', () => {
expect(apiJsonToRelease(json)).toEqual(expectedRelease);
});
});
+
+ describe('convertGraphQLResponse', () => {
+ let graphqlReleasesResponse;
+ let converted;
+
+ beforeEach(() => {
+ graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
+ converted = convertGraphQLResponse(graphqlReleasesResponse);
+ });
+
+ it('matches snapshot', () => {
+ expect(converted).toMatchSnapshot();
+ });
+
+ describe('assets', () => {
+ it("handles asset links that don't have a linkType", () => {
+ expect(converted.data[0].assets.links[0].linkType).not.toBeUndefined();
+
+ delete graphqlReleasesResponse.data.project.releases.nodes[0].assets.links.nodes[0]
+ .linkType;
+
+ converted = convertGraphQLResponse(graphqlReleasesResponse);
+
+ expect(converted.data[0].assets.links[0].linkType).toBeUndefined();
+ });
+ });
+
+ describe('_links', () => {
+ it("handles releases that don't have any links", () => {
+ expect(converted.data[0]._links.selfUrl).not.toBeUndefined();
+
+ delete graphqlReleasesResponse.data.project.releases.nodes[0].links;
+
+ converted = convertGraphQLResponse(graphqlReleasesResponse);
+
+ expect(converted.data[0]._links.selfUrl).toBeUndefined();
+ });
+ });
+
+ describe('commit', () => {
+ it("handles releases that don't have any commit info", () => {
+ expect(converted.data[0].commit).not.toBeUndefined();
+
+ delete graphqlReleasesResponse.data.project.releases.nodes[0].commit;
+
+ converted = convertGraphQLResponse(graphqlReleasesResponse);
+
+ expect(converted.data[0].commit).toBeUndefined();
+ });
+ });
+ });
});
diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
index a036588596a..ccceb78f2d1 100644
--- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
+++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
@@ -2,7 +2,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
-import store from '~/reports/accessibility_report/store';
+import { getStoreConfig } from '~/reports/accessibility_report/store';
import { mockReport } from './mock_data';
const localVue = createLocalVue();
@@ -20,16 +20,17 @@ describe('Grouped accessibility reports app', () => {
propsData: {
endpoint: 'endpoint.json',
},
- methods: {
- fetchReport: () => {},
- },
});
};
const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]');
beforeEach(() => {
- mockStore = store();
+ mockStore = new Vuex.Store({
+ ...getStoreConfig(),
+ actions: { fetchReport: () => {}, setEndpoint: () => {} },
+ });
+
mountComponent();
});
diff --git a/spec/frontend/reports/accessibility_report/mock_data.js b/spec/frontend/reports/accessibility_report/mock_data.js
index 20ad01bd802..9dace1e7c54 100644
--- a/spec/frontend/reports/accessibility_report/mock_data.js
+++ b/spec/frontend/reports/accessibility_report/mock_data.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const mockReport = {
status: 'failed',
summary: {
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
index 1905ca0d5e1..77d7c6f8678 100644
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -2,7 +2,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
-import store from '~/reports/codequality_report/store';
+import { getStoreConfig } from '~/reports/codequality_report/store';
import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data';
const localVue = createLocalVue();
@@ -13,21 +13,22 @@ describe('Grouped code quality reports app', () => {
let wrapper;
let mockStore;
+ const PATHS = {
+ codequalityHelpPath: 'codequality_help.html',
+ basePath: 'base.json',
+ headPath: 'head.json',
+ baseBlobPath: 'base/blob/path/',
+ headBlobPath: 'head/blob/path/',
+ };
+
const mountComponent = (props = {}) => {
wrapper = mount(Component, {
store: mockStore,
localVue,
propsData: {
- basePath: 'base.json',
- headPath: 'head.json',
- baseBlobPath: 'base/blob/path/',
- headBlobPath: 'head/blob/path/',
- codequalityHelpPath: 'codequality_help.html',
+ ...PATHS,
...props,
},
- methods: {
- fetchReports: () => {},
- },
});
};
@@ -35,7 +36,19 @@ describe('Grouped code quality reports app', () => {
const findIssueBody = () => wrapper.find(CodequalityIssueBody);
beforeEach(() => {
- mockStore = store();
+ const { state, ...storeConfig } = getStoreConfig();
+ mockStore = new Vuex.Store({
+ ...storeConfig,
+ actions: {
+ setPaths: () => {},
+ fetchReports: () => {},
+ },
+ state: {
+ ...state,
+ ...PATHS,
+ },
+ });
+
mountComponent();
});
@@ -126,7 +139,11 @@ describe('Grouped code quality reports app', () => {
});
it('renders a help icon with more information', () => {
- expect(findWidget().html()).toContain('ic-question');
+ expect(
+ findWidget()
+ .find('[data-testid="question-icon"]')
+ .exists(),
+ ).toBe(true);
});
});
@@ -140,7 +157,11 @@ describe('Grouped code quality reports app', () => {
});
it('does not render a help icon', () => {
- expect(findWidget().html()).not.toContain('ic-question');
+ expect(
+ findWidget()
+ .find('[data-testid="question-icon"]')
+ .exists(),
+ ).toBe(false);
});
});
});
diff --git a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap b/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
index 70e1ff01323..b5a4cb42463 100644
--- a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
+++ b/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
@@ -4,7 +4,7 @@ exports[`IssueStatusIcon renders "failed" state correctly 1`] = `
<div
class="report-block-list-icon failed"
>
- <icon-stub
+ <gl-icon-stub
data-qa-selector="status_failed_icon"
name="status_failed_borderless"
size="24"
@@ -16,7 +16,7 @@ exports[`IssueStatusIcon renders "neutral" state correctly 1`] = `
<div
class="report-block-list-icon neutral"
>
- <icon-stub
+ <gl-icon-stub
data-qa-selector="status_neutral_icon"
name="dash"
size="24"
@@ -28,7 +28,7 @@ exports[`IssueStatusIcon renders "success" state correctly 1`] = `
<div
class="report-block-list-icon success"
>
- <icon-stub
+ <gl-icon-stub
data-qa-selector="status_success_icon"
name="status_success_borderless"
size="24"
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js
index 1f8f4a0e4c1..1172e514707 100644
--- a/spec/frontend/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/reports/components/grouped_issues_list_spec.js
@@ -42,7 +42,7 @@ describe('Grouped Issues List', () => {
});
it.each('resolved', 'unresolved')('does not render report items for %s issues', () => {
- expect(wrapper.contains(ReportItem)).toBe(false);
+ expect(wrapper.find(ReportItem).exists()).toBe(false);
});
});
diff --git a/spec/frontend/reports/components/grouped_test_reports_app_spec.js b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
index c26e2fbc19a..556904b7da5 100644
--- a/spec/frontend/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import GroupedTestReportsApp from '~/reports/components/grouped_test_reports_app.vue';
-import store from '~/reports/store';
+import { getStoreConfig } from '~/reports/store';
import { failedReport } from '../mock_data/mock_data';
import successTestReports from '../mock_data/no_failures_report.json';
@@ -29,9 +29,6 @@ describe('Grouped test reports app', () => {
pipelinePath,
...props,
},
- methods: {
- fetchReports: () => {},
- },
});
};
@@ -49,7 +46,13 @@ describe('Grouped test reports app', () => {
wrapper.findAll('[data-testid="test-issue-body-description"]');
beforeEach(() => {
- mockStore = store();
+ mockStore = new Vuex.Store({
+ ...getStoreConfig(),
+ actions: {
+ fetchReports: () => {},
+ setEndpoint: () => {},
+ },
+ });
mountComponent();
});
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 10669330b61..1b8bbd5af6b 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui';
import Table from '~/repository/components/table/index.vue';
import TableRow from '~/repository/components/table/row.vue';
@@ -34,12 +34,13 @@ const MOCK_BLOBS = [
},
];
-function factory({ path, isLoading = false, entries = {} }) {
+function factory({ path, isLoading = false, hasMore = true, entries = {} }) {
vm = shallowMount(Table, {
propsData: {
path,
isLoading,
entries,
+ hasMore,
},
mocks: {
$apollo,
@@ -88,4 +89,27 @@ describe('Repository table component', () => {
expect(rows.length).toEqual(3);
expect(rows.at(2).attributes().mode).toEqual('120000');
});
+
+ describe('Show more button', () => {
+ const showMoreButton = () => vm.find(GlButton);
+
+ it.each`
+ hasMore | expectButtonToExist
+ ${true} | ${true}
+ ${false} | ${false}
+ `('renders correctly', ({ hasMore, expectButtonToExist }) => {
+ factory({ path: '/', hasMore });
+ expect(showMoreButton().exists()).toBe(expectButtonToExist);
+ });
+
+ it('when is clicked, emits showMore event', async () => {
+ factory({ path: '/' });
+
+ showMoreButton().vm.$emit('click');
+
+ await vm.vm.$nextTick();
+
+ expect(vm.emitted('showMore')).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index ea85cd34743..70dbfaea551 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { GlButton } from '@gitlab/ui';
import TreeContent, { INITIAL_FETCH_COUNT } from '~/repository/components/tree_content.vue';
import FilePreview from '~/repository/components/preview/index.vue';
+import FileTable from '~/repository/components/table/index.vue';
let vm;
let $apollo;
@@ -82,41 +82,36 @@ describe('Repository table component', () => {
});
});
- describe('Show more button', () => {
- const showMoreButton = () => vm.find(GlButton);
-
+ describe('FileTable showMore', () => {
describe('when is present', () => {
+ const fileTable = () => vm.find(FileTable);
+
beforeEach(async () => {
factory('/');
-
- vm.setData({ fetchCounter: 10, clickedShowMore: false });
-
- await vm.vm.$nextTick();
});
- it('is not rendered once it is clicked', async () => {
- showMoreButton().vm.$emit('click');
+ it('is changes hasShowMore to false when "showMore" event is emitted', async () => {
+ fileTable().vm.$emit('showMore');
+
await vm.vm.$nextTick();
- expect(showMoreButton().exists()).toBe(false);
+ expect(vm.vm.hasShowMore).toBe(false);
});
- it('is rendered', async () => {
- expect(showMoreButton().exists()).toBe(true);
- });
+ it('changes clickedShowMore when "showMore" event is emitted', async () => {
+ fileTable().vm.$emit('showMore');
- it('changes clickedShowMore when show more button is clicked', async () => {
- showMoreButton().vm.$emit('click');
+ await vm.vm.$nextTick();
expect(vm.vm.clickedShowMore).toBe(true);
});
- it('triggers fetchFiles when show more button is clicked', async () => {
+ it('triggers fetchFiles when "showMore" event is emitted', () => {
jest.spyOn(vm.vm, 'fetchFiles');
- showMoreButton().vm.$emit('click');
+ fileTable().vm.$emit('showMore');
- expect(vm.vm.fetchFiles).toBeCalled();
+ expect(vm.vm.fetchFiles).toHaveBeenCalled();
});
});
@@ -127,7 +122,7 @@ describe('Repository table component', () => {
await vm.vm.$nextTick();
- expect(showMoreButton().exists()).toBe(false);
+ expect(vm.vm.hasShowMore).toBe(false);
});
it('has limit of 1000 files on initial load', () => {
diff --git a/spec/frontend/repository/components/web_ide_link_spec.js b/spec/frontend/repository/components/web_ide_link_spec.js
deleted file mode 100644
index 877756db364..00000000000
--- a/spec/frontend/repository/components/web_ide_link_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { mount } from '@vue/test-utils';
-import WebIdeLink from '~/repository/components/web_ide_link.vue';
-
-describe('Web IDE link component', () => {
- let wrapper;
-
- function createComponent(props) {
- wrapper = mount(WebIdeLink, {
- propsData: { ...props },
- mocks: {
- $route: {
- params: {},
- },
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders link to the Web IDE for a project if only projectPath is given', () => {
- createComponent({ projectPath: 'gitlab-org/gitlab', refSha: 'master' });
-
- expect(wrapper.attributes('href')).toBe('/-/ide/project/gitlab-org/gitlab/edit/master/-/');
- expect(wrapper.text()).toBe('Web IDE');
- });
-
- it('renders link to the Web IDE for a project even if both projectPath and forkPath are given', () => {
- createComponent({
- projectPath: 'gitlab-org/gitlab',
- refSha: 'master',
- forkPath: 'my-namespace/gitlab',
- });
-
- expect(wrapper.attributes('href')).toBe('/-/ide/project/gitlab-org/gitlab/edit/master/-/');
- expect(wrapper.text()).toBe('Web IDE');
- });
-
- it('renders link to the forked project if it exists and cannot write to the repo', () => {
- createComponent({
- projectPath: 'gitlab-org/gitlab',
- refSha: 'master',
- forkPath: 'my-namespace/gitlab',
- canPushCode: false,
- });
-
- expect(wrapper.attributes('href')).toBe('/-/ide/project/my-namespace/gitlab/edit/master/-/');
- expect(wrapper.text()).toBe('Edit fork in Web IDE');
- });
-});
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index 5637d0be957..954424b5c8a 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -100,24 +100,27 @@ describe('fetchLogsTree', () => {
);
}));
- it('writes query to client', () =>
- fetchLogsTree(client, '', '0', resolver).then(() => {
- expect(client.writeQuery).toHaveBeenCalledWith({
- query: expect.anything(),
- data: {
- commits: [
- expect.objectContaining({
- __typename: 'LogTreeCommit',
- commitPath: 'https://test.com',
- committedDate: '2019-01-01',
- fileName: 'index.js',
- filePath: '/index.js',
- message: 'testing message',
- sha: '123',
- type: 'blob',
- }),
- ],
- },
- });
- }));
+ it('writes query to client', async () => {
+ await fetchLogsTree(client, '', '0', resolver);
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: expect.anything(),
+ data: {
+ projectPath: 'gitlab-org/gitlab-foss',
+ escapedRef: 'master',
+ commits: [
+ expect.objectContaining({
+ __typename: 'LogTreeCommit',
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ filePath: '/index.js',
+ message: 'testing message',
+ sha: '123',
+ titleHtml: undefined,
+ type: 'blob',
+ }),
+ ],
+ },
+ });
+ });
});
diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js
index f2f3dda41d9..3c7dda05ca3 100644
--- a/spec/frontend/repository/router_spec.js
+++ b/spec/frontend/repository/router_spec.js
@@ -4,12 +4,13 @@ import createRouter from '~/repository/router';
describe('Repository router spec', () => {
it.each`
- path | branch | component | componentName
- ${'/'} | ${'master'} | ${IndexPage} | ${'IndexPage'}
- ${'/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'}
- ${'/-/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'}
- ${'/-/tree/master/app/assets'} | ${'master'} | ${TreePage} | ${'TreePage'}
- ${'/-/tree/123/app/assets'} | ${'master'} | ${null} | ${'null'}
+ path | branch | component | componentName
+ ${'/'} | ${'master'} | ${IndexPage} | ${'IndexPage'}
+ ${'/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'}
+ ${'/tree/feat(test)'} | ${'feat(test)'} | ${TreePage} | ${'TreePage'}
+ ${'/-/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'}
+ ${'/-/tree/master/app/assets'} | ${'master'} | ${TreePage} | ${'TreePage'}
+ ${'/-/tree/123/app/assets'} | ${'master'} | ${null} | ${'null'}
`('sets component as $componentName for path "$path"', ({ path, component, branch }) => {
const router = createRouter('', branch);
diff --git a/spec/frontend/search/components/state_filter_spec.js b/spec/frontend/search/components/state_filter_spec.js
new file mode 100644
index 00000000000..26344f2b592
--- /dev/null
+++ b/spec/frontend/search/components/state_filter_spec.js
@@ -0,0 +1,104 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import StateFilter from '~/search/state_filter/components/state_filter.vue';
+import {
+ FILTER_STATES,
+ SCOPES,
+ FILTER_STATES_BY_SCOPE,
+ FILTER_TEXT,
+} from '~/search/state_filter/constants';
+import * as urlUtils from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ setUrlParams: jest.fn(),
+}));
+
+function createComponent(props = { scope: 'issues' }) {
+ return shallowMount(StateFilter, {
+ propsData: {
+ ...props,
+ },
+ });
+}
+
+describe('StateFilter', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
+ const findDropdownItemsText = () => findGlDropdownItems().wrappers.map(w => w.text());
+ const firstDropDownItem = () => findGlDropdownItems().at(0);
+
+ describe('template', () => {
+ describe.each`
+ scope | showStateDropdown
+ ${'issues'} | ${true}
+ ${'merge_requests'} | ${true}
+ ${'projects'} | ${false}
+ ${'milestones'} | ${false}
+ ${'users'} | ${false}
+ ${'notes'} | ${false}
+ ${'wiki_blobs'} | ${false}
+ ${'blobs'} | ${false}
+ `(`state dropdown`, ({ scope, showStateDropdown }) => {
+ beforeEach(() => {
+ wrapper = createComponent({ scope });
+ });
+
+ it(`does${showStateDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
+ expect(findGlDropdown().exists()).toBe(showStateDropdown);
+ });
+ });
+
+ describe.each`
+ state | label
+ ${FILTER_STATES.ANY.value} | ${FILTER_TEXT}
+ ${FILTER_STATES.OPEN.value} | ${FILTER_STATES.OPEN.label}
+ ${FILTER_STATES.CLOSED.value} | ${FILTER_STATES.CLOSED.label}
+ ${FILTER_STATES.MERGED.value} | ${FILTER_STATES.MERGED.label}
+ `(`filter text`, ({ state, label }) => {
+ describe(`when state is ${state}`, () => {
+ beforeEach(() => {
+ wrapper = createComponent({ scope: 'issues', state });
+ });
+
+ it(`sets dropdown label to ${label}`, () => {
+ expect(findGlDropdown().attributes('text')).toBe(label);
+ });
+ });
+ });
+
+ describe('Filter options', () => {
+ it('renders a dropdown item for each filterOption', () => {
+ expect(findDropdownItemsText()).toStrictEqual(
+ FILTER_STATES_BY_SCOPE[SCOPES.ISSUES].map(v => {
+ return v.label;
+ }),
+ );
+ });
+
+ it('clicking a dropdown item calls setUrlParams', () => {
+ const state = FILTER_STATES[Object.keys(FILTER_STATES)[0]].value;
+ firstDropDownItem().vm.$emit('click');
+
+ expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ state });
+ });
+
+ it('clicking a dropdown item calls visitUrl', () => {
+ firstDropDownItem().vm.$emit('click');
+
+ expect(urlUtils.visitUrl).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index ee46dc015af..3240664f5aa 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign */
import $ from 'jquery';
-import '~/gl_dropdown';
import AxiosMockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import initSearchAutocomplete from '~/search_autocomplete';
@@ -215,7 +214,7 @@ describe('Search autocomplete dropdown', () => {
function triggerAutocomplete() {
return new Promise(resolve => {
- const dropdown = widget.searchInput.data('glDropdown');
+ const dropdown = widget.searchInput.data('deprecatedJQueryDropdown');
const filterCallback = dropdown.filter.options.callback;
dropdown.filter.options.callback = jest.fn(data => {
filterCallback(data);
diff --git a/spec/frontend/serverless/utils.js b/spec/frontend/serverless/utils.js
index ba451b7d573..4af3eda1ffb 100644
--- a/spec/frontend/serverless/utils.js
+++ b/spec/frontend/serverless/utils.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const adjustMetricQuery = data => {
const updatedMetric = data.metrics;
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index 3d16074154c..538b3afa50f 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -1,6 +1,18 @@
import $ from 'jquery';
+import { flatten } from 'lodash';
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
+const mockMousetrap = {
+ bind: jest.fn(),
+ unbind: jest.fn(),
+};
+
+jest.mock('mousetrap', () => {
+ return jest.fn().mockImplementation(() => mockMousetrap);
+});
+
+jest.mock('mousetrap/plugins/pause/mousetrap-pause', () => {});
+
describe('Shortcuts', () => {
const fixtureName = 'snippets/show.html';
const createEvent = (type, target) =>
@@ -10,16 +22,16 @@ describe('Shortcuts', () => {
preloadFixtures(fixtureName);
- describe('toggleMarkdownPreview', () => {
- beforeEach(() => {
- loadFixtures(fixtureName);
+ beforeEach(() => {
+ loadFixtures(fixtureName);
- jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus');
- jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus');
+ jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus');
+ jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus');
- new Shortcuts(); // eslint-disable-line no-new
- });
+ new Shortcuts(); // eslint-disable-line no-new
+ });
+ describe('toggleMarkdownPreview', () => {
it('focuses preview button in form', () => {
Shortcuts.toggleMarkdownPreview(
createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text')),
@@ -43,4 +55,63 @@ describe('Shortcuts', () => {
expect(document.querySelector('.edit-note .js-md-preview-button').focus).toHaveBeenCalled();
});
});
+
+ describe('markdown shortcuts', () => {
+ let shortcuts;
+
+ beforeEach(() => {
+ // Get all shortcuts specified with md-shortcuts attributes in the fixture.
+ // `shortcuts` will look something like this:
+ // [
+ // [ 'mod+b' ],
+ // [ 'mod+i' ],
+ // [ 'mod+k' ]
+ // ]
+ shortcuts = $('.edit-note .js-md')
+ .map(function getShortcutsFromToolbarBtn() {
+ const mdShortcuts = $(this).data('md-shortcuts');
+
+ // jQuery.map() automatically unwraps arrays, so we
+ // have to double wrap the array to counteract this:
+ // https://stackoverflow.com/a/4875669/1063392
+ return mdShortcuts ? [mdShortcuts] : undefined;
+ })
+ .get();
+ });
+
+ describe('initMarkdownEditorShortcuts', () => {
+ beforeEach(() => {
+ Shortcuts.initMarkdownEditorShortcuts($('.edit-note textarea'));
+ });
+
+ it('attaches a Mousetrap handler for every markdown shortcut specified with md-shortcuts', () => {
+ const expectedCalls = shortcuts.map(s => [s, expect.any(Function)]);
+
+ expect(mockMousetrap.bind.mock.calls).toEqual(expectedCalls);
+ });
+
+ it('attaches a stopCallback that allows each markdown shortcut specified with md-shortcuts', () => {
+ flatten(shortcuts).forEach(s => {
+ expect(mockMousetrap.stopCallback(null, null, s)).toBe(false);
+ });
+ });
+ });
+
+ describe('removeMarkdownEditorShortcuts', () => {
+ it('does nothing if initMarkdownEditorShortcuts was not previous called', () => {
+ Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea'));
+
+ expect(mockMousetrap.unbind.mock.calls).toEqual([]);
+ });
+
+ it('removes Mousetrap handlers for every markdown shortcut specified with md-shortcuts', () => {
+ Shortcuts.initMarkdownEditorShortcuts($('.edit-note textarea'));
+ Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea'));
+
+ const expectedCalls = shortcuts.map(s => [s]);
+
+ expect(mockMousetrap.unbind.mock.calls).toEqual(expectedCalls);
+ });
+ });
+ });
});
diff --git a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
index 4c1ab4a499c..11ab1ca3aaa 100644
--- a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
@@ -7,13 +7,9 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
>
<div
class="sidebar-collapsed-icon"
- data-boundary="viewport"
- data-container="body"
- data-original-title="Not confidential"
- data-placement="left"
- title=""
+ title="Not confidential"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
name="eye"
size="16"
@@ -38,7 +34,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
class="no-value sidebar-item-value"
data-testid="not-confidential"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
class="sidebar-item-icon inline"
name="eye"
@@ -59,13 +55,9 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
>
<div
class="sidebar-collapsed-icon"
- data-boundary="viewport"
- data-container="body"
- data-original-title="Not confidential"
- data-placement="left"
- title=""
+ title="Not confidential"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
name="eye"
size="16"
@@ -98,7 +90,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
class="no-value sidebar-item-value"
data-testid="not-confidential"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
class="sidebar-item-icon inline"
name="eye"
@@ -119,13 +111,9 @@ exports[`Confidential Issue Sidebar Block renders for confidential = true and is
>
<div
class="sidebar-collapsed-icon"
- data-boundary="viewport"
- data-container="body"
- data-original-title="Confidential"
- data-placement="left"
- title=""
+ title="Confidential"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
name="eye-slash"
size="16"
@@ -149,7 +137,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = true and is
<div
class="value sidebar-item-value hide-collapsed"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
class="sidebar-item-icon inline is-active"
name="eye-slash"
@@ -170,13 +158,9 @@ exports[`Confidential Issue Sidebar Block renders for confidential = true and is
>
<div
class="sidebar-collapsed-icon"
- data-boundary="viewport"
- data-container="body"
- data-original-title="Confidential"
- data-placement="left"
- title=""
+ title="Confidential"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
name="eye-slash"
size="16"
@@ -208,7 +192,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = true and is
<div
class="value sidebar-item-value hide-collapsed"
>
- <icon-stub
+ <gl-icon-stub
aria-hidden="true"
class="sidebar-item-icon inline is-active"
name="eye-slash"
diff --git a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
index 0a12eb327de..42012841f0b 100644
--- a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
@@ -13,7 +13,7 @@ exports[`SidebarTodo template renders component container element with proper da
title=""
type="button"
>
- <icon-stub
+ <gl-icon-stub
class="todo-undone"
name="todo-done"
size="16"
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index 3418680f8ea..d1810ada97a 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
+import { GlIcon } from '@gitlab/ui';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
import UsersMock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
@@ -29,10 +30,12 @@ describe('Assignee component', () => {
it('displays no assignee icon when collapsed', () => {
createWrapper();
const collapsedChildren = findCollapsedChildren();
+ const userIcon = collapsedChildren.at(0).find(GlIcon);
expect(collapsedChildren.length).toBe(1);
expect(collapsedChildren.at(0).attributes('aria-label')).toBe('None');
- expect(collapsedChildren.at(0).classes()).toContain('fa', 'fa-user');
+ expect(userIcon.exists()).toBe(true);
+ expect(userIcon.props('name')).toBe('user');
});
it('displays only "None" when no users are assigned and the issue is read-only', () => {
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
index a1e19c1dd8e..907d6144415 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
@@ -20,7 +21,7 @@ describe('CollapsedAssigneeList component', () => {
});
}
- const findNoUsersIcon = () => wrapper.find('i[aria-label=None]');
+ const findNoUsersIcon = () => wrapper.find(GlIcon);
const findAvatarCounter = () => wrapper.find('.avatar-counter');
const findAssignees = () => wrapper.findAll(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('title');
@@ -38,6 +39,7 @@ describe('CollapsedAssigneeList component', () => {
it('has no users', () => {
expect(findNoUsersIcon().exists()).toBe(true);
+ expect(findNoUsersIcon().props('name')).toBe('user');
});
});
diff --git a/spec/frontend/sidebar/components/severity/severity_spec.js b/spec/frontend/sidebar/components/severity/severity_spec.js
new file mode 100644
index 00000000000..b6690f11d6b
--- /dev/null
+++ b/spec/frontend/sidebar/components/severity/severity_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
+import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+
+describe('SeverityToken', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = shallowMount(SeverityToken, {
+ propsData: {
+ ...props,
+ },
+ });
+ }
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findIcon = () => wrapper.find(GlIcon);
+
+ it('renders severity token for each severity type', () => {
+ Object.values(INCIDENT_SEVERITY).forEach(severity => {
+ createComponent({ severity });
+ expect(findIcon().classes()).toContain(`icon-${severity.icon}`);
+ expect(findIcon().attributes('name')).toBe(`severity-${severity.icon}`);
+ expect(wrapper.text()).toBe(severity.label);
+ });
+ });
+
+ it('renders only icon when `iconOnly` prop is set to `true`', () => {
+ const severity = INCIDENT_SEVERITY.CRITICAL;
+ createComponent({ severity, iconOnly: true });
+ expect(findIcon().classes()).toContain(`icon-${severity.icon}`);
+ expect(findIcon().attributes('name')).toBe(`severity-${severity.icon}`);
+ expect(wrapper.text()).toBe('');
+ });
+
+ describe('icon size', () => {
+ it('renders the icon in default size when other is not specified', () => {
+ const severity = INCIDENT_SEVERITY.HIGH;
+ createComponent({ severity });
+ expect(findIcon().attributes('size')).toBe('12');
+ });
+
+ it('renders the icon in provided size', () => {
+ const severity = INCIDENT_SEVERITY.HIGH;
+ const iconSize = 14;
+ createComponent({ severity, iconSize });
+ expect(findIcon().attributes('size')).toBe(`${iconSize}`);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
new file mode 100644
index 00000000000..638d3706d12
--- /dev/null
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -0,0 +1,166 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
+import updateIssuableSeverity from '~/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql';
+import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/components/severity/constants';
+
+jest.mock('~/flash');
+
+describe('SidebarSeverity', () => {
+ let wrapper;
+ let mutate;
+ const projectPath = 'gitlab-org/gitlab-test';
+ const iid = '1';
+ const severity = 'CRITICAL';
+
+ function createComponent(props = {}) {
+ const propsData = {
+ projectPath,
+ iid,
+ issuableType: ISSUABLE_TYPES.INCIDENT,
+ initialSeverity: severity,
+ ...props,
+ };
+ mutate = jest.fn();
+ wrapper = shallowMount(SidebarSeverity, {
+ propsData,
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findSeverityToken = () => wrapper.findAll(SeverityToken);
+ const findEditBtn = () => wrapper.find('[data-testid="editButton"]');
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findCriticalSeverityDropdownItem = () => wrapper.find(GlDropdownItem);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findTooltip = () => wrapper.find(GlTooltip);
+ const findCollapsedSeverity = () => wrapper.find({ ref: 'severity' });
+
+ it('renders severity widget', () => {
+ expect(findEditBtn().exists()).toBe(true);
+ expect(findSeverityToken().exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ describe('Update severity', () => {
+ it('calls `$apollo.mutate` with `updateIssuableSeverity`', () => {
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValueOnce({ data: { issueSetSeverity: { issue: { severity } } } });
+
+ findCriticalSeverityDropdownItem().vm.$emit('click');
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: updateIssuableSeverity,
+ variables: {
+ iid,
+ projectPath,
+ severity,
+ },
+ });
+ });
+
+ it('shows error alert when severity update fails ', () => {
+ const errorMsg = 'Something went wrong';
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValueOnce(errorMsg);
+ findCriticalSeverityDropdownItem().vm.$emit('click');
+
+ setImmediate(() => {
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ it('shows loading icon while updating', async () => {
+ let resolvePromise;
+ wrapper.vm.$apollo.mutate = jest.fn(
+ () =>
+ new Promise(resolve => {
+ resolvePromise = resolve;
+ }),
+ );
+ findCriticalSeverityDropdownItem().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ resolvePromise();
+ await waitForPromises();
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('Switch between collapsed/expanded view of the sidebar', () => {
+ const HIDDDEN_CLASS = 'gl-display-none';
+ const SHOWN_CLASS = 'show';
+
+ describe('collapsed', () => {
+ it('should have collapsed icon class', () => {
+ expect(findCollapsedSeverity().classes('sidebar-collapsed-icon')).toBe(true);
+ });
+
+ it('should display only icon with a tooltip', () => {
+ expect(
+ findSeverityToken()
+ .at(0)
+ .attributes('icononly'),
+ ).toBe('true');
+ expect(
+ findSeverityToken()
+ .at(0)
+ .attributes('iconsize'),
+ ).toBe('14');
+ expect(
+ findTooltip()
+ .text()
+ .replace(/\s+/g, ' '),
+ ).toContain(`Severity: ${INCIDENT_SEVERITY[severity].label}`);
+ });
+
+ it('should expand the dropdown on collapsed icon click', async () => {
+ wrapper.vm.isDropdownShowing = false;
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
+
+ findCollapsedSeverity().trigger('click');
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().classes(SHOWN_CLASS)).toBe(true);
+ });
+ });
+
+ describe('expanded', () => {
+ it('toggles dropdown with edit button', async () => {
+ wrapper.vm.isDropdownShowing = false;
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
+
+ findEditBtn().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().classes(SHOWN_CLASS)).toBe(true);
+
+ findEditBtn().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/issuable_assignees_spec.js
new file mode 100644
index 00000000000..aa930bd4198
--- /dev/null
+++ b/spec/frontend/sidebar/issuable_assignees_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+
+describe('IssuableAssignees', () => {
+ let wrapper;
+
+ const createComponent = (props = { users: [] }) => {
+ wrapper = shallowMount(IssuableAssignees, {
+ provide: {
+ rootPath: '',
+ },
+ propsData: { ...props },
+ });
+ };
+ const findLabel = () => wrapper.find('[data-testid="assigneeLabel"');
+ const findUncollapsedAssigneeList = () => wrapper.find(UncollapsedAssigneeList);
+ const findEmptyAssignee = () => wrapper.find('[data-testid="none"]');
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when no assignees are present', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders "None"', () => {
+ expect(findEmptyAssignee().text()).toBe('None');
+ });
+
+ it('renders "0 assignees"', () => {
+ expect(findLabel().text()).toBe('0 Assignees');
+ });
+ });
+
+ describe('when assignees are present', () => {
+ it('renders UncollapsedAssigneesList', () => {
+ createComponent({ users: [{ id: 1 }] });
+
+ expect(findUncollapsedAssigneeList().exists()).toBe(true);
+ });
+
+ it.each`
+ assignees | expected
+ ${[{ id: 1 }]} | ${'Assignee'}
+ ${[{ id: 1 }, { id: 2 }]} | ${'2 Assignees'}
+ `(
+ 'when assignees have a length of $assignees.length, it renders $expected',
+ ({ assignees, expected }) => {
+ createComponent({ users: assignees });
+
+ expect(findLabel().text()).toBe(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
index ebe94582588..93c9b3b84c3 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -37,7 +37,7 @@ describe('Participants', () => {
loading: true,
});
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('does not show loading spinner not loading', () => {
@@ -45,7 +45,7 @@ describe('Participants', () => {
loading: false,
});
- expect(wrapper.contains(GlLoadingIcon)).toBe(false);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('shows participant count when given', () => {
@@ -74,7 +74,7 @@ describe('Participants', () => {
loading: true,
});
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
@@ -196,11 +196,11 @@ describe('Participants', () => {
});
it('does not show sidebar collapsed icon', () => {
- expect(wrapper.contains('.sidebar-collapsed-icon')).toBe(false);
+ expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(false);
});
it('does not show participants label title', () => {
- expect(wrapper.contains('.title')).toBe(false);
+ expect(wrapper.find('.title').exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/sidebar_labels_spec.js b/spec/frontend/sidebar/sidebar_labels_spec.js
new file mode 100644
index 00000000000..29333a344e1
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_labels_spec.js
@@ -0,0 +1,124 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
+import {
+ mockLabels,
+ mockRegularLabel,
+} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+import axios from '~/lib/utils/axios_utils';
+import SidebarLabels from '~/sidebar/components/labels/sidebar_labels.vue';
+import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('sidebar labels', () => {
+ let axiosMock;
+ let wrapper;
+
+ const store = new Vuex.Store(labelsSelectModule());
+
+ const defaultProps = {
+ allowLabelCreate: true,
+ allowLabelEdit: true,
+ allowScopedLabels: true,
+ canEdit: true,
+ iid: '1',
+ initiallySelectedLabels: mockLabels,
+ issuableType: 'issue',
+ labelsFetchPath: '/gitlab-org/gitlab-test/-/labels.json?include_ancestor_groups=true',
+ labelsManagePath: '/gitlab-org/gitlab-test/-/labels',
+ labelsUpdatePath: '/gitlab-org/gitlab-test/-/issues/1.json',
+ projectIssuesPath: '/gitlab-org/gitlab-test/-/issues',
+ projectPath: 'gitlab-org/gitlab-test',
+ };
+
+ const findLabelsSelect = () => wrapper.find(LabelsSelect);
+
+ const mountComponent = () => {
+ wrapper = shallowMount(SidebarLabels, {
+ localVue,
+ provide: {
+ ...defaultProps,
+ },
+ store,
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ axiosMock.restore();
+ });
+
+ describe('LabelsSelect props', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('are as expected', () => {
+ expect(findLabelsSelect().props()).toMatchObject({
+ allowLabelCreate: defaultProps.allowLabelCreate,
+ allowLabelEdit: defaultProps.allowLabelEdit,
+ allowMultiselect: true,
+ allowScopedLabels: defaultProps.allowScopedLabels,
+ footerCreateLabelTitle: 'Create project label',
+ footerManageLabelTitle: 'Manage project labels',
+ labelsCreateTitle: 'Create project label',
+ labelsFetchPath: defaultProps.labelsFetchPath,
+ labelsFilterBasePath: defaultProps.projectIssuesPath,
+ labelsManagePath: defaultProps.labelsManagePath,
+ labelsSelectInProgress: false,
+ selectedLabels: defaultProps.initiallySelectedLabels,
+ variant: DropdownVariant.Sidebar,
+ });
+ });
+ });
+
+ describe('when labels are changed', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('makes an API call to update labels', async () => {
+ const labels = [
+ {
+ ...mockRegularLabel,
+ set: false,
+ },
+ {
+ id: 40,
+ title: 'Security',
+ color: '#ddd',
+ text_color: '#fff',
+ set: true,
+ },
+ {
+ id: 55,
+ title: 'Tooling',
+ color: '#ddd',
+ text_color: '#fff',
+ set: false,
+ },
+ ];
+
+ findLabelsSelect().vm.$emit('updateSelectedLabels', labels);
+
+ await axios.waitForAll();
+
+ const expected = {
+ [defaultProps.issuableType]: {
+ label_ids: [27, 28, 40],
+ },
+ };
+
+ expect(axiosMock.history.put[0].data).toEqual(JSON.stringify(expected));
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
index db0d3e06272..ad919f69546 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -68,12 +68,10 @@ describe('SidebarMoveIssue', () => {
});
describe('initDropdown', () => {
- it('should initialize the gl_dropdown', () => {
- jest.spyOn($.fn, 'glDropdown').mockImplementation(() => {});
-
+ it('should initialize the deprecatedJQueryDropdown', () => {
test.sidebarMoveIssue.initDropdown();
- expect($.fn.glDropdown).toHaveBeenCalled();
+ expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeTruthy();
});
it('escapes html from project name', done => {
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js
index cce35666985..dddb9c2bba9 100644
--- a/spec/frontend/sidebar/subscriptions_spec.js
+++ b/spec/frontend/sidebar/subscriptions_spec.js
@@ -100,7 +100,7 @@ describe('Subscriptions', () => {
});
it('does not render the toggle button', () => {
- expect(wrapper.contains('.js-issuable-subscribe-button')).toBe(false);
+ expect(wrapper.find('.js-issuable-subscribe-button').exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index e56a78989eb..b0e94f16dd7 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -1,8 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import SidebarTodos from '~/sidebar/components/todo_toggle/todo.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const defaultProps = {
issuableId: 1,
@@ -45,11 +44,11 @@ describe('SidebarTodo', () => {
expect(
wrapper
- .find(Icon)
+ .find(GlIcon)
.classes()
.join(' '),
).toStrictEqual(iconClass);
- expect(wrapper.find(Icon).props('name')).toStrictEqual(icon);
+ expect(wrapper.find(GlIcon).props('name')).toStrictEqual(icon);
expect(wrapper.find('button').text()).toBe(label);
},
);
@@ -82,7 +81,7 @@ describe('SidebarTodo', () => {
it('renders button icon when `collapsed` prop is `true`', () => {
createComponent({ collapsed: true });
- expect(wrapper.find(Icon).props('name')).toBe('todo-done');
+ expect(wrapper.find(GlIcon).props('name')).toBe('todo-done');
});
it('renders loading icon when `isActionActive` prop is true', () => {
@@ -94,7 +93,7 @@ describe('SidebarTodo', () => {
it('hides button icon when `isActionActive` prop is true', () => {
createComponent({ collapsed: true, isActionActive: true });
- expect(wrapper.find(Icon).isVisible()).toBe(false);
+ expect(wrapper.find(GlIcon).isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/snippet/snippet_bundle_spec.js b/spec/frontend/snippet/snippet_bundle_spec.js
index ad69a91fe89..208d2fea804 100644
--- a/spec/frontend/snippet/snippet_bundle_spec.js
+++ b/spec/frontend/snippet/snippet_bundle_spec.js
@@ -15,11 +15,13 @@ describe('Snippet editor', () => {
const updatedMockContent = 'New Foo Bar';
const mockEditor = {
- createInstance: jest.fn(),
updateModelLanguage: jest.fn(),
getValue: jest.fn().mockReturnValueOnce(updatedMockContent),
};
- Editor.mockImplementation(() => mockEditor);
+ const createInstance = jest.fn().mockImplementation(() => ({ ...mockEditor }));
+ Editor.mockImplementation(() => ({
+ createInstance,
+ }));
function setUpFixture(name, content) {
setHTMLFixture(`
@@ -56,7 +58,7 @@ describe('Snippet editor', () => {
});
it('correctly initializes Editor', () => {
- expect(mockEditor.createInstance).toHaveBeenCalledWith({
+ expect(createInstance).toHaveBeenCalledWith({
el: editorEl,
blobPath: mockName,
blobContent: mockContent,
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 6020d595e3f..3b101e9e815 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -41,7 +41,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
>
<textarea
aria-label="Description"
- class="note-textarea js-gfm-input js-autosize markdown-area"
+ class="note-textarea js-gfm-input js-autosize markdown-area js-gfm-input-initialized"
data-qa-selector="snippet_description_field"
data-supports-quick-actions="false"
dir="auto"
@@ -63,8 +63,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
href="#"
>
- <icon-stub
- name="screen-normal"
+ <gl-icon-stub
+ name="minimize"
size="16"
/>
</a>
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
index be75a5bfbdc..8446f0f50c4 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
@@ -20,6 +20,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</label>
<gl-form-group-stub
+ class="gl-mb-0"
id="visibility-level-setting"
>
<gl-form-radio-group-stub
@@ -90,5 +91,12 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub>
</gl-form-radio-group-stub>
</gl-form-group-stub>
+
+ <div
+ class="text-muted"
+ data-testid="restricted-levels-info"
+ >
+ <!---->
+ </div>
</div>
`;
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index ebab6aa84f6..b6abb9f389a 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -47,6 +47,8 @@ const createTestSnippet = () => ({
describe('Snippet Edit app', () => {
let wrapper;
+ const relativeUrlRoot = '/foo/';
+ const originalRelativeUrlRoot = gon.relative_url_root;
const mutationTypes = {
RESOLVE: jest.fn().mockResolvedValue({
@@ -100,16 +102,25 @@ describe('Snippet Edit app', () => {
markdownDocsPath: 'http://docs.foo.bar',
...props,
},
+ data() {
+ return {
+ snippet: {
+ visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+ },
+ };
+ },
});
}
beforeEach(() => {
+ gon.relative_url_root = relativeUrlRoot;
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ gon.relative_url_root = originalRelativeUrlRoot;
});
const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
@@ -164,10 +175,10 @@ describe('Snippet Edit app', () => {
props => {
createComponent(props);
- expect(wrapper.contains(TitleField)).toBe(true);
- expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
- expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
- expect(wrapper.contains(FormFooterActions)).toBe(true);
+ expect(wrapper.find(TitleField).exists()).toBe(true);
+ expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
+ expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
+ expect(wrapper.find(FormFooterActions).exists()).toBe(true);
expect(findBlobActions().exists()).toBe(true);
},
);
@@ -196,8 +207,8 @@ describe('Snippet Edit app', () => {
it.each`
projectPath | snippetArg | expectation
- ${''} | ${[]} | ${'/-/snippets'}
- ${'project/path'} | ${[]} | ${'/project/path/-/snippets'}
+ ${''} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
+ ${'project/path'} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
`(
diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js
new file mode 100644
index 00000000000..8eb44965692
--- /dev/null
+++ b/spec/frontend/snippets/components/embed_dropdown_spec.js
@@ -0,0 +1,70 @@
+import { escape as esc } from 'lodash';
+import { mount } from '@vue/test-utils';
+import { GlFormInputGroup } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
+
+const TEST_URL = `${TEST_HOST}/test/no">'xss`;
+
+describe('snippets/components/embed_dropdown', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mount(EmbedDropdown, {
+ propsData: {
+ url: TEST_URL,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findSectionsData = () => {
+ const sections = [];
+ let current = {};
+
+ wrapper.findAll('[data-testid="header"],[data-testid="input"]').wrappers.forEach(x => {
+ const type = x.attributes('data-testid');
+
+ if (type === 'header') {
+ current = {
+ header: x.text(),
+ };
+
+ sections.push(current);
+ } else {
+ const value = x.find(GlFormInputGroup).props('value');
+ const copyValue = x.find('button[title="Copy"]').attributes('data-clipboard-text');
+
+ Object.assign(current, {
+ value,
+ copyValue,
+ });
+ }
+ });
+
+ return sections;
+ };
+
+ it('renders dropdown items', () => {
+ createComponent();
+
+ const embedValue = `<script src="${esc(TEST_URL)}.js"></script>`;
+
+ expect(findSectionsData()).toEqual([
+ {
+ header: 'Embed',
+ value: embedValue,
+ copyValue: embedValue,
+ },
+ {
+ header: 'Share',
+ value: TEST_URL,
+ copyValue: TEST_URL,
+ },
+ ]);
+ });
+});
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index 8cccbb83d54..b5ab7def753 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -2,7 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { shallowMount } from '@vue/test-utils';
import SnippetApp from '~/snippets/components/show.vue';
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
+import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
@@ -57,7 +57,7 @@ describe('Snippet view app', () => {
expect(wrapper.find(SnippetTitle).exists()).toBe(true);
});
- it('renders embeddable component if visibility allows', () => {
+ it('renders embed dropdown component if visibility allows', () => {
createComponent({
data: {
snippet: {
@@ -66,7 +66,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.contains(BlobEmbeddable)).toBe(true);
+ expect(wrapper.find(EmbedDropdown).exists()).toBe(true);
});
it('renders correct snippet-blob components', () => {
@@ -88,7 +88,7 @@ describe('Snippet view app', () => {
${SNIPPET_VISIBILITY_PRIVATE} | ${'not render'} | ${false}
${'foo'} | ${'not render'} | ${false}
${SNIPPET_VISIBILITY_PUBLIC} | ${'render'} | ${true}
- `('does $condition blob-embeddable by default', ({ visibilityLevel, isRendered }) => {
+ `('does $condition embed-dropdown by default', ({ visibilityLevel, isRendered }) => {
createComponent({
data: {
snippet: {
@@ -97,7 +97,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.contains(BlobEmbeddable)).toBe(isRendered);
+ expect(wrapper.find(EmbedDropdown).exists()).toBe(isRendered);
});
});
@@ -119,7 +119,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.contains(CloneDropdownButton)).toBe(isRendered);
+ expect(wrapper.find(CloneDropdownButton).exists()).toBe(isRendered);
},
);
});
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 188f9ae5cf1..fc4da46d722 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -17,6 +17,7 @@ const TEST_PATH = 'foo/bar/test.md';
const TEST_RAW_PATH = '/gitlab/raw/path/to/blob/7';
const TEST_FULL_PATH = joinPaths(TEST_HOST, TEST_RAW_PATH);
const TEST_CONTENT = 'Lorem ipsum dolar sit amet,\nconsectetur adipiscing elit.';
+const TEST_JSON_CONTENT = '{"abc":"lorem ipsum"}';
const TEST_BLOB = {
id: TEST_ID,
@@ -66,7 +67,7 @@ describe('Snippet Blob Edit component', () => {
});
describe('with not loaded blob', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
@@ -100,6 +101,20 @@ describe('Snippet Blob Edit component', () => {
});
});
+ describe('with unloaded blob and JSON content', () => {
+ beforeEach(() => {
+ axiosMock.onGet(TEST_FULL_PATH).reply(200, TEST_JSON_CONTENT);
+ createComponent();
+ });
+
+ // This checks against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/241199
+ it('emits raw content', async () => {
+ await waitForPromises();
+
+ expect(getLastUpdatedArgs()).toEqual({ content: TEST_JSON_CONTENT });
+ });
+ });
+
describe('with error', () => {
beforeEach(() => {
axiosMock.reset();
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index da8cb2e6a8d..5836de1fdbe 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -5,6 +5,7 @@ import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import waitForPromises from 'helpers/wait_for_promises';
import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
describe('Snippet header component', () => {
let wrapper;
@@ -14,6 +15,7 @@ describe('Snippet header component', () => {
let errorMsg;
let err;
+ const originalRelativeUrlRoot = gon.relative_url_root;
function createComponent({
loading = false,
@@ -50,6 +52,7 @@ describe('Snippet header component', () => {
}
beforeEach(() => {
+ gon.relative_url_root = '/foo/';
snippet = {
id: 'gid://gitlab/PersonalSnippet/50',
title: 'The property of Thor',
@@ -65,7 +68,7 @@ describe('Snippet header component', () => {
name: 'Thor Odinson',
},
blobs: [Blob],
- createdAt: new Date(Date.now() - 32 * 24 * 3600 * 1000).toISOString(),
+ createdAt: new Date(differenceInMilliseconds(32 * 24 * 3600 * 1000)).toISOString(),
};
mutationVariables = {
@@ -86,6 +89,7 @@ describe('Snippet header component', () => {
afterEach(() => {
wrapper.destroy();
+ gon.relative_url_root = originalRelativeUrlRoot;
});
it('renders itself', () => {
@@ -213,7 +217,7 @@ describe('Snippet header component', () => {
it('redirects to dashboard/snippets for personal snippet', () => {
return createDeleteSnippet().then(() => {
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
- expect(window.location.pathname).toBe('dashboard/snippets');
+ expect(window.location.pathname).toBe(`${gon.relative_url_root}dashboard/snippets`);
});
});
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index a8df13787a5..3919e4d7993 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -1,31 +1,55 @@
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
+import { defaultSnippetVisibilityLevels } from '~/snippets/utils/blob';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
+ SNIPPET_LEVELS_RESTRICTED,
+ SNIPPET_LEVELS_DISABLED,
} from '~/snippets/constants';
describe('Snippet Visibility Edit component', () => {
let wrapper;
const defaultHelpLink = '/foo/bar';
const defaultVisibilityLevel = 'private';
-
- function createComponent(propsData = {}, deep = false) {
+ const defaultVisibility = defaultSnippetVisibilityLevels([0, 10, 20]);
+
+ function createComponent({
+ propsData = {},
+ visibilityLevels = defaultVisibility,
+ multipleLevelsRestricted = false,
+ deep = false,
+ } = {}) {
const method = deep ? mount : shallowMount;
+ const $apollo = {
+ queries: {
+ defaultVisibility: {
+ loading: false,
+ },
+ },
+ };
+
wrapper = method.call(this, SnippetVisibilityEdit, {
+ mock: { $apollo },
propsData: {
helpLink: defaultHelpLink,
isProjectSnippet: false,
value: defaultVisibilityLevel,
...propsData,
},
+ data() {
+ return {
+ visibilityLevels,
+ multipleLevelsRestricted,
+ };
+ },
});
}
- const findLabel = () => wrapper.find('label');
+ const findLink = () => wrapper.find('label').find(GlLink);
const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
const findRadiosData = () =>
findRadios().wrappers.map(x => {
@@ -47,56 +71,84 @@ describe('Snippet Visibility Edit component', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('renders visibility options', () => {
- createComponent({}, true);
+ it('renders label help link', () => {
+ createComponent();
+
+ expect(findLink().attributes('href')).toBe(defaultHelpLink);
+ });
+
+ it('when helpLink is not defined, does not render label help link', () => {
+ createComponent({ propsData: { helpLink: null } });
- expect(findRadiosData()).toEqual([
- {
+ expect(findLink().exists()).toBe(false);
+ });
+
+ describe('Visibility options', () => {
+ const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]');
+ const RESULTING_OPTIONS = {
+ 0: {
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description,
},
- {
+ 10: {
value: SNIPPET_VISIBILITY_INTERNAL,
icon: SNIPPET_VISIBILITY.internal.icon,
text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description,
},
- {
+ 20: {
value: SNIPPET_VISIBILITY_PUBLIC,
icon: SNIPPET_VISIBILITY.public.icon,
text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description,
},
- ]);
- });
-
- it('when project snippet, renders special private description', () => {
- createComponent({ isProjectSnippet: true }, true);
+ };
- expect(findRadiosData()[0]).toEqual({
- value: SNIPPET_VISIBILITY_PRIVATE,
- icon: SNIPPET_VISIBILITY.private.icon,
- text: SNIPPET_VISIBILITY.private.label,
- description: SNIPPET_VISIBILITY.private.description_project,
+ it.each`
+ levels | resultOptions
+ ${undefined} | ${[]}
+ ${''} | ${[]}
+ ${[]} | ${[]}
+ ${[0]} | ${[RESULTING_OPTIONS[0]]}
+ ${[0, 10]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10]]}
+ ${[0, 10, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
+ ${[0, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[20]]}
+ ${[10, 20]} | ${[RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
+ `('renders correct visibility options for $levels', ({ levels, resultOptions }) => {
+ createComponent({ visibilityLevels: defaultSnippetVisibilityLevels(levels), deep: true });
+ expect(findRadiosData()).toEqual(resultOptions);
});
- });
- it('renders label help link', () => {
- createComponent();
-
- expect(
- findLabel()
- .find(GlLink)
- .attributes('href'),
- ).toBe(defaultHelpLink);
- });
+ it.each`
+ levels | levelsRestricted | resultText
+ ${[]} | ${false} | ${SNIPPET_LEVELS_DISABLED}
+ ${[]} | ${true} | ${SNIPPET_LEVELS_DISABLED}
+ ${[0]} | ${true} | ${SNIPPET_LEVELS_RESTRICTED}
+ ${[0]} | ${false} | ${''}
+ ${[0, 10, 20]} | ${false} | ${''}
+ `(
+ 'renders correct information about restricted visibility levels for $levels',
+ ({ levels, levelsRestricted, resultText }) => {
+ createComponent({
+ visibilityLevels: defaultSnippetVisibilityLevels(levels),
+ multipleLevelsRestricted: levelsRestricted,
+ });
+ expect(findRestrictedInfo().text()).toBe(resultText);
+ },
+ );
- it('when helpLink is not defined, does not render label help link', () => {
- createComponent({ helpLink: null });
+ it('when project snippet, renders special private description', () => {
+ createComponent({ propsData: { isProjectSnippet: true }, deep: true });
- expect(findLabel().contains(GlLink)).toBe(false);
+ expect(findRadiosData()[0]).toEqual({
+ value: SNIPPET_VISIBILITY_PRIVATE,
+ icon: SNIPPET_VISIBILITY.private.icon,
+ text: SNIPPET_VISIBILITY.private.label,
+ description: SNIPPET_VISIBILITY.private.description_project,
+ });
+ });
});
});
@@ -104,7 +156,7 @@ describe('Snippet Visibility Edit component', () => {
it('pre-selects correct option in the list', () => {
const value = SNIPPET_VISIBILITY_INTERNAL;
- createComponent({ value });
+ createComponent({ propsData: { value } });
expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
});
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
index f4be911171e..7e90b53dd07 100644
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -6,11 +6,13 @@ import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/consta
import EditArea from '~/static_site_editor/components/edit_area.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
+import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
import {
sourceContentTitle as title,
- sourceContent as content,
+ sourceContentYAML as content,
+ sourceContentHeaderObjYAML as headerSettings,
sourceContentBody as body,
returnUrl,
} from '../mock_data';
@@ -36,6 +38,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
};
const findEditHeader = () => wrapper.find(EditHeader);
+ const findEditDrawer = () => wrapper.find(EditDrawer);
const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findUnsavedChangesConfirmDialog = () => wrapper.find(UnsavedChangesConfirmDialog);
@@ -46,6 +49,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
it('renders edit header', () => {
@@ -53,6 +57,10 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
expect(findEditHeader().props('title')).toBe(title);
});
+ it('renders edit drawer', () => {
+ expect(findEditDrawer().exists()).toBe(true);
+ });
+
it('renders rich content editor with a format pass', () => {
expect(findRichContentEditor().exists()).toBe(true);
expect(findRichContentEditor().props('content')).toBe(formattedBody);
@@ -81,7 +89,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
it('updates parsedSource with new content', () => {
const newContent = 'New content';
- const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'sync');
+ const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncContent');
findRichContentEditor().vm.$emit('input', newContent);
@@ -148,11 +156,88 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
});
});
+ describe('when content has front matter', () => {
+ it('renders a closed edit drawer', () => {
+ expect(findEditDrawer().exists()).toBe(true);
+ expect(findEditDrawer().props('isOpen')).toBe(false);
+ });
+
+ it('opens the edit drawer', () => {
+ findPublishToolbar().vm.$emit('editSettings');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEditDrawer().props('isOpen')).toBe(true);
+ });
+ });
+
+ it('closes the edit drawer', () => {
+ findEditDrawer().vm.$emit('close');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEditDrawer().props('isOpen')).toBe(false);
+ });
+ });
+
+ it('forwards the matter settings when the drawer is open', () => {
+ findPublishToolbar().vm.$emit('editSettings');
+
+ jest.spyOn(wrapper.vm.parsedSource, 'matter').mockReturnValueOnce(headerSettings);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEditDrawer().props('settings')).toEqual(headerSettings);
+ });
+ });
+
+ it('enables toolbar submit button', () => {
+ expect(findPublishToolbar().props('hasSettings')).toBe(true);
+ });
+
+ it('syncs matter changes regardless of edit mode', () => {
+ const newSettings = { title: 'test' };
+ const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncMatter');
+
+ findEditDrawer().vm.$emit('updateSettings', newSettings);
+
+ expect(spySyncParsedSource).toHaveBeenCalledWith(newSettings);
+ });
+
+ it('syncs matter changes to content in markdown mode', () => {
+ wrapper.setData({ editorMode: EDITOR_TYPES.markdown });
+
+ const newSettings = { title: 'test' };
+
+ findEditDrawer().vm.$emit('updateSettings', newSettings);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findRichContentEditor().props('content')).toContain('title: test');
+ });
+ });
+ });
+
+ describe('when content lacks front matter', () => {
+ beforeEach(() => {
+ buildWrapper({ content: body });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not render edit drawer', () => {
+ expect(findEditDrawer().exists()).toBe(false);
+ });
+
+ it('does not enable toolbar submit button', () => {
+ expect(findPublishToolbar().props('hasSettings')).toBe(false);
+ });
+ });
+
describe('when content is submitted', () => {
it('should format the content', () => {
findPublishToolbar().vm.$emit('submit', content);
expect(wrapper.emitted('submit')[0][0].content).toBe(`${content} format-pass format-pass`);
+ expect(wrapper.emitted('submit').length).toBe(1);
});
});
});
diff --git a/spec/frontend/static_site_editor/components/edit_drawer_spec.js b/spec/frontend/static_site_editor/components/edit_drawer_spec.js
new file mode 100644
index 00000000000..c47eef59997
--- /dev/null
+++ b/spec/frontend/static_site_editor/components/edit_drawer_spec.js
@@ -0,0 +1,68 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlDrawer } from '@gitlab/ui';
+
+import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
+import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
+
+describe('~/static_site_editor/components/edit_drawer.vue', () => {
+ let wrapper;
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = shallowMount(EditDrawer, {
+ propsData: {
+ isOpen: false,
+ settings: { title: 'Some title' },
+ ...propsData,
+ },
+ });
+ };
+
+ const findFrontMatterControls = () => wrapper.find(FrontMatterControls);
+ const findGlDrawer = () => wrapper.find(GlDrawer);
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders the GlDrawer', () => {
+ expect(findGlDrawer().exists()).toBe(true);
+ });
+
+ it('renders the FrontMatterControls', () => {
+ expect(findFrontMatterControls().exists()).toBe(true);
+ });
+
+ it('forwards the settings to FrontMatterControls', () => {
+ expect(findFrontMatterControls().props('settings')).toBe(wrapper.props('settings'));
+ });
+
+ it('is closed by default', () => {
+ expect(findGlDrawer().props('open')).toBe(false);
+ });
+
+ it('can open', () => {
+ buildWrapper({ isOpen: true });
+
+ expect(findGlDrawer().props('open')).toBe(true);
+ });
+
+ it.each`
+ event | payload | finderFn
+ ${'close'} | ${undefined} | ${findGlDrawer}
+ ${'updateSettings'} | ${{ some: 'data' }} | ${findFrontMatterControls}
+ `(
+ 'forwards the emitted $event event from the $finderFn with $payload',
+ ({ event, payload, finderFn }) => {
+ finderFn().vm.$emit(event, payload);
+
+ expect(wrapper.emitted(event)[0][0]).toBe(payload);
+ expect(wrapper.emitted(event).length).toBe(1);
+ },
+ );
+});
diff --git a/spec/frontend/static_site_editor/components/front_matter_controls_spec.js b/spec/frontend/static_site_editor/components/front_matter_controls_spec.js
new file mode 100644
index 00000000000..82e8fad643e
--- /dev/null
+++ b/spec/frontend/static_site_editor/components/front_matter_controls_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlFormGroup } from '@gitlab/ui';
+import { humanize } from '~/lib/utils/text_utility';
+
+import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
+
+describe('~/static_site_editor/components/front_matter_controls.vue', () => {
+ let wrapper;
+
+ // TODO Refactor and update `sourceContentHeaderObjYAML` in mock_data when !41230 lands
+ const settings = {
+ layout: 'handbook-page-toc',
+ title: 'Handbook',
+ twitter_image: '/images/tweets/handbook-gitlab.png',
+ suppress_header: true,
+ extra_css: ['sales-and-free-trial-common.css', 'form-to-resource.css'],
+ };
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = shallowMount(FrontMatterControls, {
+ propsData: {
+ settings,
+ ...propsData,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render only the supported GlFormGroup types', () => {
+ expect(wrapper.findAll(GlFormGroup)).toHaveLength(3);
+ });
+
+ it.each`
+ key
+ ${'layout'}
+ ${'title'}
+ ${'twitter_image'}
+ `('renders field when key is $key', ({ key }) => {
+ const glFormGroup = wrapper.find(`#sse-front-matter-form-group-${key}`);
+ const glFormInput = wrapper.find(`#sse-front-matter-control-${key}`);
+
+ expect(glFormGroup.exists()).toBe(true);
+ expect(glFormGroup.attributes().label).toBe(humanize(key));
+
+ expect(glFormInput.exists()).toBe(true);
+ expect(glFormInput.attributes().value).toBe(settings[key]);
+ });
+
+ it.each`
+ key
+ ${'suppress_header'}
+ ${'extra_css'}
+ `('does not render field when key is $key', ({ key }) => {
+ const glFormInput = wrapper.find(`#sse-front-matter-control-${key}`);
+
+ expect(glFormInput.exists()).toBe(false);
+ });
+
+ it('emits updated settings when nested control updates', () => {
+ const elId = `#sse-front-matter-control-title`;
+ const glFormInput = wrapper.find(elId);
+ const newTitle = 'New title';
+
+ glFormInput.vm.$emit('input', newTitle);
+
+ const newSettings = { ...settings, title: newTitle };
+
+ expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings);
+ });
+});
diff --git a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
index 5428ed23266..9ba7e4a94d1 100644
--- a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
+++ b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlButton } from '@gitlab/ui';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
@@ -11,6 +10,7 @@ describe('Static Site Editor Toolbar', () => {
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(PublishToolbar, {
propsData: {
+ hasSettings: false,
saveable: false,
...propsData,
},
@@ -18,7 +18,8 @@ describe('Static Site Editor Toolbar', () => {
};
const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' });
- const findSaveChangesButton = () => wrapper.find(GlButton);
+ const findSaveChangesButton = () => wrapper.find({ ref: 'submit' });
+ const findEditSettingsButton = () => wrapper.find({ ref: 'settings' });
beforeEach(() => {
buildWrapper();
@@ -28,6 +29,10 @@ describe('Static Site Editor Toolbar', () => {
wrapper.destroy();
});
+ it('does not render Settings button', () => {
+ expect(findEditSettingsButton().exists()).toBe(false);
+ });
+
it('renders Submit Changes button', () => {
expect(findSaveChangesButton().exists()).toBe(true);
});
@@ -51,6 +56,14 @@ describe('Static Site Editor Toolbar', () => {
expect(findReturnUrlLink().attributes('href')).toBe(returnUrl);
});
+ describe('when providing settings CTA', () => {
+ it('enables Submit Changes button', () => {
+ buildWrapper({ hasSettings: true });
+
+ expect(findEditSettingsButton().exists()).toBe(true);
+ });
+ });
+
describe('when saveable', () => {
it('enables Submit Changes button', () => {
buildWrapper({ saveable: true });
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js
index 8504d09e0f1..24651543650 100644
--- a/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js
+++ b/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js
@@ -5,7 +5,7 @@ import {
projectId,
sourcePath,
sourceContentTitle as title,
- sourceContent as content,
+ sourceContentYAML as content,
} from '../../mock_data';
jest.mock('~/static_site_editor/services/load_source_content', () => jest.fn());
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
index 515b5394594..750b777cf5d 100644
--- a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
+++ b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
@@ -6,7 +6,7 @@ import {
projectId as project,
sourcePath,
username,
- sourceContent as content,
+ sourceContentYAML as content,
savedContentMeta,
} from '../../mock_data';
diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js
index 96de9b73af0..d861f6c9cd7 100644
--- a/spec/frontend/static_site_editor/mock_data.js
+++ b/spec/frontend/static_site_editor/mock_data.js
@@ -1,19 +1,22 @@
-export const sourceContentHeader = `---
+export const sourceContentHeaderYAML = `---
layout: handbook-page-toc
title: Handbook
-twitter_image: '/images/tweets/handbook-gitlab.png'
+twitter_image: /images/tweets/handbook-gitlab.png
---`;
-export const sourceContentSpacing = `
-`;
+export const sourceContentHeaderObjYAML = {
+ layout: 'handbook-page-toc',
+ title: 'Handbook',
+ twitter_image: '/images/tweets/handbook-gitlab.png',
+};
+export const sourceContentSpacing = `\n`;
export const sourceContentBody = `## On this page
{:.no_toc .hidden-md .hidden-lg}
- TOC
{:toc .hidden-md .hidden-lg}
-![image](path/to/image1.png)
-`;
-export const sourceContent = `${sourceContentHeader}${sourceContentSpacing}${sourceContentBody}`;
+![image](path/to/image1.png)`;
+export const sourceContentYAML = `${sourceContentHeaderYAML}${sourceContentSpacing}${sourceContentBody}`;
export const sourceContentTitle = 'Handbook';
export const username = 'gitlabuser';
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
index c5473596df8..41f8a1075c0 100644
--- a/spec/frontend/static_site_editor/pages/home_spec.js
+++ b/spec/frontend/static_site_editor/pages/home_spec.js
@@ -13,7 +13,7 @@ import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constant
import {
projectId as project,
returnUrl,
- sourceContent as content,
+ sourceContentYAML as content,
sourceContentTitle as title,
sourcePath,
username,
diff --git a/spec/frontend/static_site_editor/services/formatter_spec.js b/spec/frontend/static_site_editor/services/formatter_spec.js
index b7600798db9..9e9c4bbd171 100644
--- a/spec/frontend/static_site_editor/services/formatter_spec.js
+++ b/spec/frontend/static_site_editor/services/formatter_spec.js
@@ -1,6 +1,6 @@
import formatter from '~/static_site_editor/services/formatter';
-describe('formatter', () => {
+describe('static_site_editor/services/formatter', () => {
const source = `Some text
<br>
@@ -23,4 +23,17 @@ And even more text`;
it('removes extraneous <br> tags', () => {
expect(formatter(source)).toMatch(sourceWithoutBrTags);
});
+
+ describe('ordered lists with incorrect content indentation', () => {
+ it.each`
+ input | result
+ ${'12. ordered list item\n13.Next ordered list item'} | ${'12. ordered list item\n13.Next ordered list item'}
+ ${'12. ordered list item\n - Next ordered list item'} | ${'12. ordered list item\n - Next ordered list item'}
+ ${'12. ordered list item\n - Next ordered list item'} | ${'12. ordered list item\n - Next ordered list item'}
+ ${'12. ordered list item\n Next ordered list item'} | ${'12. ordered list item\n Next ordered list item'}
+ ${'1. ordered list item\n Next ordered list item'} | ${'1. ordered list item\n Next ordered list item'}
+ `('\ntransforms\n$input \nto\n$result', ({ input, result }) => {
+ expect(formatter(input)).toBe(result);
+ });
+ });
});
diff --git a/spec/frontend/static_site_editor/services/load_source_content_spec.js b/spec/frontend/static_site_editor/services/load_source_content_spec.js
index 87893bb7a6e..54061b7a503 100644
--- a/spec/frontend/static_site_editor/services/load_source_content_spec.js
+++ b/spec/frontend/static_site_editor/services/load_source_content_spec.js
@@ -2,7 +2,12 @@ import Api from '~/api';
import loadSourceContent from '~/static_site_editor/services/load_source_content';
-import { sourceContent, sourceContentTitle, projectId, sourcePath } from '../mock_data';
+import {
+ sourceContentYAML as sourceContent,
+ sourceContentTitle,
+ projectId,
+ sourcePath,
+} from '../mock_data';
describe('loadSourceContent', () => {
describe('requesting source content succeeds', () => {
diff --git a/spec/frontend/static_site_editor/services/parse_source_file_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
index 4588548e614..ab9e63f4cd2 100644
--- a/spec/frontend/static_site_editor/services/parse_source_file_spec.js
+++ b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
@@ -1,14 +1,29 @@
-import { sourceContent as content, sourceContentBody as body } from '../mock_data';
+import {
+ sourceContentYAML as content,
+ sourceContentHeaderYAML as yamlFrontMatter,
+ sourceContentHeaderObjYAML as yamlFrontMatterObj,
+ sourceContentBody as body,
+} from '../mock_data';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
-describe('parseSourceFile', () => {
+describe('static_site_editor/services/parse_source_file', () => {
const contentComplex = [content, content, content].join('');
const complexBody = [body, content, content].join('');
const edit = 'and more';
const newContent = `${content} ${edit}`;
const newContentComplex = `${contentComplex} ${edit}`;
+ describe('unmodified front matter', () => {
+ it.each`
+ parsedSource
+ ${parseSourceFile(content)}
+ ${parseSourceFile(contentComplex)}
+ `('returns $targetFrontMatter when frontMatter queried', ({ parsedSource }) => {
+ expect(parsedSource.matter()).toEqual(yamlFrontMatterObj);
+ });
+ });
+
describe('unmodified content', () => {
it.each`
parsedSource
@@ -34,21 +49,50 @@ describe('parseSourceFile', () => {
);
});
+ describe('modified front matter', () => {
+ const newYamlFrontMatter = '---\nnewKey: newVal\n---';
+ const newYamlFrontMatterObj = { newKey: 'newVal' };
+ const contentWithNewFrontMatter = content.replace(yamlFrontMatter, newYamlFrontMatter);
+ const contentComplexWithNewFrontMatter = contentComplex.replace(
+ yamlFrontMatter,
+ newYamlFrontMatter,
+ );
+
+ it.each`
+ parsedSource | targetContent
+ ${parseSourceFile(content)} | ${contentWithNewFrontMatter}
+ ${parseSourceFile(contentComplex)} | ${contentComplexWithNewFrontMatter}
+ `(
+ 'returns the correct front matter and modified content',
+ ({ parsedSource, targetContent }) => {
+ expect(parsedSource.matter()).toMatchObject(yamlFrontMatterObj);
+
+ parsedSource.syncMatter(newYamlFrontMatterObj);
+
+ expect(parsedSource.matter()).toMatchObject(newYamlFrontMatterObj);
+ expect(parsedSource.content()).toBe(targetContent);
+ },
+ );
+ });
+
describe('modified content', () => {
const newBody = `${body} ${edit}`;
const newComplexBody = `${complexBody} ${edit}`;
it.each`
- parsedSource | isModified | targetRaw | targetBody
- ${parseSourceFile(content)} | ${false} | ${content} | ${body}
- ${parseSourceFile(content)} | ${true} | ${newContent} | ${newBody}
- ${parseSourceFile(contentComplex)} | ${false} | ${contentComplex} | ${complexBody}
- ${parseSourceFile(contentComplex)} | ${true} | ${newContentComplex} | ${newComplexBody}
+ parsedSource | hasMatter | isModified | targetRaw | targetBody
+ ${parseSourceFile(content)} | ${true} | ${false} | ${content} | ${body}
+ ${parseSourceFile(content)} | ${true} | ${true} | ${newContent} | ${newBody}
+ ${parseSourceFile(contentComplex)} | ${true} | ${false} | ${contentComplex} | ${complexBody}
+ ${parseSourceFile(contentComplex)} | ${true} | ${true} | ${newContentComplex} | ${newComplexBody}
+ ${parseSourceFile(body)} | ${false} | ${false} | ${body} | ${body}
+ ${parseSourceFile(body)} | ${false} | ${true} | ${newBody} | ${newBody}
`(
'returns $isModified after a $targetRaw sync',
- ({ parsedSource, isModified, targetRaw, targetBody }) => {
- parsedSource.sync(targetRaw);
+ ({ parsedSource, hasMatter, isModified, targetRaw, targetBody }) => {
+ parsedSource.syncContent(targetRaw);
+ expect(parsedSource.hasMatter()).toBe(hasMatter);
expect(parsedSource.isModified()).toBe(isModified);
expect(parsedSource.content()).toBe(targetRaw);
expect(parsedSource.content(true)).toBe(targetBody);
diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
index 645ccedf7e7..d464e6b1895 100644
--- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
+++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
@@ -20,7 +20,7 @@ import {
commitMultipleResponse,
createMergeRequestResponse,
sourcePath,
- sourceContent as content,
+ sourceContentYAML as content,
trackingCategory,
images,
} from '../mock_data';
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
new file mode 100644
index 00000000000..0edc5248629
--- /dev/null
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -0,0 +1,202 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlTooltip } from '@gitlab/ui';
+import { useMockMutationObserver } from 'helpers/mock_dom_observer';
+import Tooltips from '~/tooltips/components/tooltips.vue';
+
+describe('tooltips/components/tooltips.vue', () => {
+ const { trigger: triggerMutate, observersCount } = useMockMutationObserver();
+ let wrapper;
+
+ const buildWrapper = () => {
+ wrapper = shallowMount(Tooltips);
+ };
+
+ const createTooltipTarget = (attributes = {}) => {
+ const target = document.createElement('button');
+ const defaults = {
+ title: 'default title',
+ ...attributes,
+ };
+
+ Object.keys(defaults).forEach(name => {
+ target.setAttribute(name, defaults[name]);
+ });
+
+ document.body.appendChild(target);
+
+ return target;
+ };
+
+ const allTooltips = () => wrapper.findAll(GlTooltip);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('addTooltips', () => {
+ let target;
+
+ beforeEach(() => {
+ buildWrapper();
+
+ target = createTooltipTarget();
+ });
+
+ it('attaches tooltips to the targets specified', async () => {
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).props('target')).toBe(target);
+ });
+
+ it('does not attach a tooltip twice to the same element', async () => {
+ wrapper.vm.addTooltips([target]);
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.findAll(GlTooltip)).toHaveLength(1);
+ });
+
+ it('sets tooltip content from title attribute', async () => {
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).text()).toBe(target.getAttribute('title'));
+ });
+
+ it('supports HTML content', async () => {
+ target = createTooltipTarget({
+ title: 'content with <b>HTML</b>',
+ 'data-html': true,
+ });
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).html()).toContain(target.getAttribute('title'));
+ });
+
+ it.each`
+ attribute | value | prop
+ ${'data-placement'} | ${'bottom'} | ${'placement'}
+ ${'data-container'} | ${'custom-container'} | ${'container'}
+ ${'data-boundary'} | ${'viewport'} | ${'boundary'}
+ ${'data-triggers'} | ${'manual'} | ${'triggers'}
+ `(
+ 'sets $prop to $value when $attribute is set in target',
+ async ({ attribute, value, prop }) => {
+ target = createTooltipTarget({ [attribute]: value });
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).props(prop)).toBe(value);
+ },
+ );
+ });
+
+ describe('dispose', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ it('removes all tooltips when elements is nil', async () => {
+ wrapper.vm.addTooltips([createTooltipTarget(), createTooltipTarget()]);
+ await wrapper.vm.$nextTick();
+
+ wrapper.vm.dispose();
+ await wrapper.vm.$nextTick();
+
+ expect(allTooltips()).toHaveLength(0);
+ });
+
+ it('removes the tooltips that target the elements specified', async () => {
+ const target = createTooltipTarget();
+
+ wrapper.vm.addTooltips([target, createTooltipTarget()]);
+ await wrapper.vm.$nextTick();
+
+ wrapper.vm.dispose(target);
+ await wrapper.vm.$nextTick();
+
+ expect(allTooltips()).toHaveLength(1);
+ });
+ });
+
+ describe('observe', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ it('removes tooltip when target is removed from the document', async () => {
+ const target = createTooltipTarget();
+
+ wrapper.vm.addTooltips([target, createTooltipTarget()]);
+ await wrapper.vm.$nextTick();
+
+ triggerMutate(document.body, {
+ entry: { removedNodes: [target] },
+ options: { childList: true },
+ });
+ await wrapper.vm.$nextTick();
+
+ expect(allTooltips()).toHaveLength(1);
+ });
+ });
+
+ describe('triggerEvent', () => {
+ it('triggers a bootstrap-vue tooltip global event for the tooltip specified', async () => {
+ const target = createTooltipTarget();
+ const event = 'hide';
+
+ buildWrapper();
+
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.vm.triggerEvent(target, event);
+
+ expect(wrapper.find(GlTooltip).emitted(event)).toHaveLength(1);
+ });
+ });
+
+ describe('fixTitle', () => {
+ it('updates tooltip content with the latest value the target title property', async () => {
+ const target = createTooltipTarget();
+ const currentTitle = 'title';
+ const newTitle = 'new title';
+
+ target.setAttribute('title', currentTitle);
+
+ buildWrapper();
+
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).text()).toBe(currentTitle);
+
+ target.setAttribute('title', newTitle);
+ wrapper.vm.fixTitle(target);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).text()).toBe(newTitle);
+ });
+ });
+
+ it('disconnects mutation observer on beforeDestroy', () => {
+ buildWrapper();
+ wrapper.vm.addTooltips([createTooltipTarget()]);
+
+ expect(observersCount()).toBe(1);
+
+ wrapper.destroy();
+ expect(observersCount()).toBe(0);
+ });
+});
diff --git a/spec/frontend/tooltips/index_spec.js b/spec/frontend/tooltips/index_spec.js
new file mode 100644
index 00000000000..cc72adee57d
--- /dev/null
+++ b/spec/frontend/tooltips/index_spec.js
@@ -0,0 +1,149 @@
+import jQuery from 'jquery';
+import { initTooltips, dispose, destroy, hide, show, enable, disable, fixTitle } from '~/tooltips';
+
+describe('tooltips/index.js', () => {
+ let tooltipsApp;
+
+ const createTooltipTarget = () => {
+ const target = document.createElement('button');
+ const attributes = {
+ title: 'default title',
+ };
+
+ Object.keys(attributes).forEach(name => {
+ target.setAttribute(name, attributes[name]);
+ });
+
+ target.classList.add('has-tooltip');
+
+ document.body.appendChild(target);
+
+ return target;
+ };
+
+ const buildTooltipsApp = () => {
+ tooltipsApp = initTooltips({ selector: '.has-tooltip' });
+ };
+
+ const triggerEvent = (target, eventName = 'mouseenter') => {
+ const event = new Event(eventName);
+
+ target.dispatchEvent(event);
+ };
+
+ beforeEach(() => {
+ window.gon.glTooltipsEnabled = true;
+ });
+
+ afterEach(() => {
+ document.body.childNodes.forEach(node => node.remove());
+ destroy();
+ });
+
+ describe('initTooltip', () => {
+ it('attaches a GlTooltip for the elements specified in the selector', async () => {
+ const target = createTooltipTarget();
+
+ buildTooltipsApp();
+
+ triggerEvent(target);
+
+ await tooltipsApp.$nextTick();
+
+ expect(document.querySelector('.gl-tooltip')).not.toBe(null);
+ expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
+ });
+
+ it('supports triggering a tooltip in custom events', async () => {
+ const target = createTooltipTarget();
+
+ buildTooltipsApp();
+ triggerEvent(target, 'click');
+
+ await tooltipsApp.$nextTick();
+
+ expect(document.querySelector('.gl-tooltip')).not.toBe(null);
+ expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
+ });
+ });
+
+ describe('dispose', () => {
+ it('removes tooltips that target the elements specified', async () => {
+ const target = createTooltipTarget();
+
+ buildTooltipsApp();
+ triggerEvent(target);
+
+ await tooltipsApp.$nextTick();
+
+ expect(document.querySelector('.gl-tooltip')).not.toBe(null);
+
+ dispose([target]);
+
+ await tooltipsApp.$nextTick();
+
+ expect(document.querySelector('.gl-tooltip')).toBe(null);
+ });
+ });
+
+ it.each`
+ methodName | method | event
+ ${'enable'} | ${enable} | ${'enable'}
+ ${'disable'} | ${disable} | ${'disable'}
+ ${'hide'} | ${hide} | ${'close'}
+ ${'show'} | ${show} | ${'open'}
+ `(
+ '$methodName calls triggerEvent in tooltip app with $event event',
+ async ({ method, event }) => {
+ const target = createTooltipTarget();
+
+ buildTooltipsApp();
+
+ await tooltipsApp.$nextTick();
+
+ jest.spyOn(tooltipsApp, 'triggerEvent');
+
+ method([target]);
+
+ expect(tooltipsApp.triggerEvent).toHaveBeenCalledWith(target, event);
+ },
+ );
+
+ it('fixTitle calls fixTitle in tooltip app with the target specified', async () => {
+ const target = createTooltipTarget();
+
+ buildTooltipsApp();
+
+ await tooltipsApp.$nextTick();
+
+ jest.spyOn(tooltipsApp, 'fixTitle');
+
+ fixTitle([target]);
+
+ expect(tooltipsApp.fixTitle).toHaveBeenCalledWith(target);
+ });
+
+ describe('when glTooltipsEnabled feature flag is disabled', () => {
+ beforeEach(() => {
+ window.gon.glTooltipsEnabled = false;
+ });
+
+ it.each`
+ method | methodName | bootstrapParams
+ ${dispose} | ${'dispose'} | ${'dispose'}
+ ${fixTitle} | ${'fixTitle'} | ${'_fixTitle'}
+ ${enable} | ${'enable'} | ${'enable'}
+ ${disable} | ${'disable'} | ${'disable'}
+ ${hide} | ${'hide'} | ${'hide'}
+ ${show} | ${'show'} | ${'show'}
+ `('delegates $methodName to bootstrap tooltip API', ({ method, bootstrapParams }) => {
+ const elements = jQuery(createTooltipTarget());
+
+ jest.spyOn(jQuery.fn, 'tooltip');
+
+ method(elements);
+
+ expect(elements.tooltip).toHaveBeenCalledWith(bootstrapParams);
+ });
+ });
+});
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index 8acfa655c2c..e2d39ffeaf0 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -1,5 +1,5 @@
import { setHTMLFixture } from './helpers/fixtures';
-import Tracking, { initUserTracking } from '~/tracking';
+import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
describe('Tracking', () => {
let snowplowSpy;
@@ -17,11 +17,6 @@ describe('Tracking', () => {
});
describe('initUserTracking', () => {
- beforeEach(() => {
- bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
- trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
- });
-
it('calls through to get a new tracker with the expected options', () => {
initUserTracking();
expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', {
@@ -38,9 +33,16 @@ describe('Tracking', () => {
linkClickTracking: false,
});
});
+ });
+
+ describe('initDefaultTrackers', () => {
+ beforeEach(() => {
+ bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
+ trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
+ });
it('should activate features based on what has been enabled', () => {
- initUserTracking();
+ initDefaultTrackers();
expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
expect(snowplowSpy).toHaveBeenCalledWith('trackPageView');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
@@ -52,18 +54,18 @@ describe('Tracking', () => {
linkClickTracking: true,
};
- initUserTracking();
+ initDefaultTrackers();
expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking');
});
it('binds the document event handling', () => {
- initUserTracking();
+ initDefaultTrackers();
expect(bindDocumentSpy).toHaveBeenCalled();
});
it('tracks page loaded events', () => {
- initUserTracking();
+ initDefaultTrackers();
expect(trackLoadEventsSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mock_data.js b/spec/frontend/vue_mr_widget/components/mock_data.js
index 39c7d75cda5..73e254f2b1a 100644
--- a/spec/frontend/vue_mr_widget/components/mock_data.js
+++ b/spec/frontend/vue_mr_widget/components/mock_data.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const artifactsList = [
{
text: 'result.txt',
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js
index 60f970e0018..4e3e918f7fb 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js
@@ -20,8 +20,8 @@ describe('MrWidgetContainer', () => {
it('has layout', () => {
factory();
- expect(wrapper.is('.mr-widget-heading')).toBe(true);
- expect(wrapper.contains('.mr-widget-content')).toBe(true);
+ expect(wrapper.classes()).toContain('mr-widget-heading');
+ expect(wrapper.find('.mr-widget-content').exists()).toBe(true);
});
it('accepts default slot', () => {
@@ -31,7 +31,7 @@ describe('MrWidgetContainer', () => {
},
});
- expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
+ expect(wrapper.find('.mr-widget-content .test-body').exists()).toBe(true);
});
it('accepts footer slot', () => {
@@ -42,7 +42,7 @@ describe('MrWidgetContainer', () => {
},
});
- expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
- expect(wrapper.contains('.test-footer')).toBe(true);
+ expect(wrapper.find('.mr-widget-content .test-body').exists()).toBe(true);
+ expect(wrapper.find('.test-footer').exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
index 69a50899d4d..3e111cd308a 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js
@@ -18,7 +18,7 @@ describe('MrWidgetExpanableSection', () => {
});
it('renders Icon', () => {
- expect(wrapper.contains(GlIcon)).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
});
it('renders header slot', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
index 21058005d29..caea9a757ae 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
@@ -25,10 +25,14 @@ describe('MRWidgetHeader', () => {
const downloadEmailPatchesEl = vm.$el.querySelector('.js-download-email-patches');
const downloadPlainDiffEl = vm.$el.querySelector('.js-download-plain-diff');
- expect(downloadEmailPatchesEl.textContent.trim()).toEqual('Email patches');
- expect(downloadEmailPatchesEl.getAttribute('href')).toEqual('/mr/email-patches');
- expect(downloadPlainDiffEl.textContent.trim()).toEqual('Plain diff');
- expect(downloadPlainDiffEl.getAttribute('href')).toEqual('/mr/plainDiffPath');
+ expect(downloadEmailPatchesEl.innerText.trim()).toEqual('Email patches');
+ expect(downloadEmailPatchesEl.querySelector('a').getAttribute('href')).toEqual(
+ '/mr/email-patches',
+ );
+ expect(downloadPlainDiffEl.innerText.trim()).toEqual('Plain diff');
+ expect(downloadPlainDiffEl.querySelector('a').getAttribute('href')).toEqual(
+ '/mr/plainDiffPath',
+ );
};
describe('computed', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
index cee0b9b0118..ea8b33495ab 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const TEST_ICON = 'commit';
@@ -21,6 +21,6 @@ describe('MrWidgetIcon', () => {
it('renders icon and container', () => {
expect(wrapper.is('.circle-icon-container')).toBe(true);
- expect(wrapper.find(Icon).props('name')).toEqual(TEST_ICON);
+ expect(wrapper.find(GlIcon).props('name')).toEqual(TEST_ICON);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 6486826c3ec..7ecd8629607 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -29,6 +29,8 @@ describe('MRWidgetPipeline', () => {
const findAllPipelineStages = () => wrapper.findAll(PipelineStage);
const findPipelineCoverage = () => wrapper.find('[data-testid="pipeline-coverage"]');
const findPipelineCoverageDelta = () => wrapper.find('[data-testid="pipeline-coverage-delta"]');
+ const findPipelineCoverageTooltipText = () =>
+ wrapper.find('[data-testid="pipeline-coverage-tooltip"]').text();
const findMonitoringPipelineMessage = () =>
wrapper.find('[data-testid="monitoring-pipeline-message"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
@@ -49,257 +51,208 @@ describe('MRWidgetPipeline', () => {
}
});
- describe('computed', () => {
- describe('hasPipeline', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('should return true when there is a pipeline', () => {
- expect(wrapper.vm.hasPipeline).toBe(true);
- });
+ it('should render CI error if there is a pipeline, but no status', () => {
+ createWrapper({ ciStatus: null }, mount);
+ expect(findCIErrorMessage().text()).toBe(ciErrorMessage);
+ });
- it('should return false when there is no pipeline', async () => {
- wrapper.setProps({ pipeline: {} });
+ it('should render a loading state when no pipeline is found', () => {
+ createWrapper({ pipeline: {} }, mount);
- await wrapper.vm.$nextTick();
+ expect(findMonitoringPipelineMessage().text()).toBe(monitoringMessage);
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
- expect(wrapper.vm.hasPipeline).toBe(false);
+ describe('with a pipeline', () => {
+ beforeEach(() => {
+ createWrapper({
+ pipelineCoverageDelta: mockData.pipelineCoverageDelta,
+ buildsWithCoverage: mockData.buildsWithCoverage,
});
});
- describe('hasCIError', () => {
- beforeEach(() => {
- createWrapper();
- });
+ it('should render pipeline ID', () => {
+ expect(
+ findPipelineID()
+ .text()
+ .trim(),
+ ).toBe(`#${mockData.pipeline.id}`);
+ });
- it('should return false when there is no CI error', () => {
- expect(wrapper.vm.hasCIError).toBe(false);
- });
+ it('should render pipeline status and commit id', () => {
+ expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
- it('should return true when there is a pipeline, but no ci status', async () => {
- wrapper.setProps({ ciStatus: null });
+ expect(
+ findCommitLink()
+ .text()
+ .trim(),
+ ).toBe(mockData.pipeline.commit.short_id);
- await wrapper.vm.$nextTick();
+ expect(findCommitLink().attributes('href')).toBe(mockData.pipeline.commit.commit_path);
+ });
- expect(wrapper.vm.hasCIError).toBe(true);
- });
+ it('should render pipeline graph', () => {
+ expect(findPipelineGraph().exists()).toBe(true);
+ expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
});
- describe('coverageDeltaClass', () => {
- beforeEach(() => {
- createWrapper({ pipelineCoverageDelta: '0' });
+ describe('should render pipeline coverage information', () => {
+ it('should render coverage percentage', () => {
+ expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
});
- it('should return no class if there is no coverage change', async () => {
- expect(wrapper.vm.coverageDeltaClass).toBe('');
+ it('should render coverage delta', () => {
+ expect(findPipelineCoverageDelta().exists()).toBe(true);
+ expect(findPipelineCoverageDelta().text()).toBe(`(${mockData.pipelineCoverageDelta}%)`);
});
- it('should return text-success if the coverage increased', async () => {
- wrapper.setProps({ pipelineCoverageDelta: '10' });
-
- await wrapper.vm.$nextTick();
+ it('coverage delta should have no special style if there is no coverage change', () => {
+ createWrapper({ pipelineCoverageDelta: '0' });
+ expect(findPipelineCoverageDelta().classes()).toEqual([]);
+ });
- expect(wrapper.vm.coverageDeltaClass).toBe('text-success');
+ it('coverage delta should have text-success style if coverage increased', () => {
+ createWrapper({ pipelineCoverageDelta: '10' });
+ expect(findPipelineCoverageDelta().classes()).toEqual(['text-success']);
});
- it('should return text-danger if the coverage decreased', async () => {
- wrapper.setProps({ pipelineCoverageDelta: '-12' });
+ it('coverage delta should have text-danger style if coverage increased', () => {
+ createWrapper({ pipelineCoverageDelta: '-10' });
+ expect(findPipelineCoverageDelta().classes()).toEqual(['text-danger']);
+ });
- await wrapper.vm.$nextTick();
+ it('should render tooltip for jobs contributing to code coverage', () => {
+ const tooltipText = findPipelineCoverageTooltipText();
+ const expectedDescription = `Coverage value for this pipeline was calculated by averaging the resulting coverage values of ${mockData.buildsWithCoverage.length} jobs.`;
- expect(wrapper.vm.coverageDeltaClass).toBe('text-danger');
+ expect(tooltipText).toContain(expectedDescription);
});
+
+ it.each(mockData.buildsWithCoverage)(
+ 'should have name and coverage for build %s listed in tooltip',
+ build => {
+ const tooltipText = findPipelineCoverageTooltipText();
+
+ expect(tooltipText).toContain(`${build.name} (${build.coverage}%)`);
+ },
+ );
});
});
- describe('rendered output', () => {
+ describe('without commit path', () => {
beforeEach(() => {
- createWrapper({ ciStatus: null }, mount);
- });
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ delete mockCopy.pipeline.commit;
- it('should render CI error if there is a pipeline, but no status', async () => {
- expect(findCIErrorMessage().text()).toBe(ciErrorMessage);
+ createWrapper({});
});
- it('should render a loading state when no pipeline is found', async () => {
- wrapper.setProps({
- pipeline: {},
- hasCi: false,
- pipelineMustSucceed: true,
- });
-
- await wrapper.vm.$nextTick();
-
- expect(findMonitoringPipelineMessage().text()).toBe(monitoringMessage);
- expect(findLoadingIcon().exists()).toBe(true);
+ it('should render pipeline ID', () => {
+ expect(
+ findPipelineID()
+ .text()
+ .trim(),
+ ).toBe(`#${mockData.pipeline.id}`);
});
- describe('with a pipeline', () => {
- beforeEach(() => {
- createWrapper(
- {
- pipelineCoverageDelta: mockData.pipelineCoverageDelta,
- },
- mount,
- );
- });
-
- it('should render pipeline ID', () => {
- expect(
- findPipelineID()
- .text()
- .trim(),
- ).toBe(`#${mockData.pipeline.id}`);
- });
-
- it('should render pipeline status and commit id', () => {
- expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
-
- expect(
- findCommitLink()
- .text()
- .trim(),
- ).toBe(mockData.pipeline.commit.short_id);
-
- expect(findCommitLink().attributes('href')).toBe(mockData.pipeline.commit.commit_path);
- });
-
- it('should render pipeline graph', () => {
- expect(findPipelineGraph().exists()).toBe(true);
- expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
- });
-
- it('should render coverage information', () => {
- expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
- });
+ it('should render pipeline status', () => {
+ expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
+ });
- it('should render pipeline coverage delta information', () => {
- expect(findPipelineCoverageDelta().exists()).toBe(true);
- expect(findPipelineCoverageDelta().text()).toBe(`(${mockData.pipelineCoverageDelta}%)`);
- });
+ it('should render pipeline graph', () => {
+ expect(findPipelineGraph().exists()).toBe(true);
+ expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
});
- describe('without commit path', () => {
- beforeEach(() => {
- const mockCopy = JSON.parse(JSON.stringify(mockData));
- delete mockCopy.pipeline.commit;
+ it('should render coverage information', () => {
+ expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
+ });
+ });
- createWrapper({}, mount);
- });
+ describe('without coverage', () => {
+ beforeEach(() => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ delete mockCopy.pipeline.coverage;
- it('should render pipeline ID', () => {
- expect(
- findPipelineID()
- .text()
- .trim(),
- ).toBe(`#${mockData.pipeline.id}`);
- });
+ createWrapper({ pipeline: mockCopy.pipeline });
+ });
- it('should render pipeline status', () => {
- expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
- });
+ it('should not render a coverage component', () => {
+ expect(findPipelineCoverage().exists()).toBe(false);
+ });
+ });
- it('should render pipeline graph', () => {
- expect(findPipelineGraph().exists()).toBe(true);
- expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
- });
+ describe('without a pipeline graph', () => {
+ beforeEach(() => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ delete mockCopy.pipeline.details.stages;
- it('should render coverage information', () => {
- expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
+ createWrapper({
+ pipeline: mockCopy.pipeline,
});
});
- describe('without coverage', () => {
- beforeEach(() => {
- const mockCopy = JSON.parse(JSON.stringify(mockData));
- delete mockCopy.pipeline.coverage;
-
- createWrapper(
- {
- pipeline: mockCopy.pipeline,
- },
- mount,
- );
- });
-
- it('should not render a coverage component', () => {
- expect(findPipelineCoverage().exists()).toBe(false);
- });
+ it('should not render a pipeline graph', () => {
+ expect(findPipelineGraph().exists()).toBe(false);
});
+ });
- describe('without a pipeline graph', () => {
- beforeEach(() => {
- const mockCopy = JSON.parse(JSON.stringify(mockData));
- delete mockCopy.pipeline.details.stages;
+ describe('for each type of pipeline', () => {
+ let pipeline;
- createWrapper({
- pipeline: mockCopy.pipeline,
- });
- });
+ beforeEach(() => {
+ ({ pipeline } = JSON.parse(JSON.stringify(mockData)));
- it('should not render a pipeline graph', () => {
- expect(findPipelineGraph().exists()).toBe(false);
- });
+ pipeline.details.name = 'Pipeline';
+ pipeline.merge_request_event_type = undefined;
+ pipeline.ref.tag = false;
+ pipeline.ref.branch = false;
});
- describe('for each type of pipeline', () => {
- let pipeline;
-
- beforeEach(() => {
- ({ pipeline } = JSON.parse(JSON.stringify(mockData)));
-
- pipeline.details.name = 'Pipeline';
- pipeline.merge_request_event_type = undefined;
- pipeline.ref.tag = false;
- pipeline.ref.branch = false;
+ const factory = () => {
+ createWrapper({
+ pipeline,
+ sourceBranchLink: mockData.source_branch_link,
});
+ };
- const factory = () => {
- createWrapper({
- pipeline,
- sourceBranchLink: mockData.source_branch_link,
- });
- };
-
- describe('for a branch pipeline', () => {
- it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
- pipeline.ref.branch = true;
+ describe('for a branch pipeline', () => {
+ it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
+ pipeline.ref.branch = true;
- factory();
+ factory();
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockData.source_branch_link}`;
- const actual = trimText(findPipelineInfoContainer().text());
+ const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockData.source_branch_link}`;
+ const actual = trimText(findPipelineInfoContainer().text());
- expect(actual).toBe(expected);
- });
+ expect(actual).toBe(expected);
});
+ });
- describe('for a tag pipeline', () => {
- it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
- pipeline.ref.tag = true;
+ describe('for a tag pipeline', () => {
+ it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
+ pipeline.ref.tag = true;
- factory();
+ factory();
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
- const actual = trimText(findPipelineInfoContainer().text());
+ const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
+ const actual = trimText(findPipelineInfoContainer().text());
- expect(actual).toBe(expected);
- });
+ expect(actual).toBe(expected);
});
+ });
- describe('for a detached merge request pipeline', () => {
- it('renders a pipeline widget that reads "Detached merge request pipeline <ID> <status> for <SHA>"', () => {
- pipeline.details.name = 'Detached merge request pipeline';
- pipeline.merge_request_event_type = 'detached';
+ describe('for a detached merge request pipeline', () => {
+ it('renders a pipeline widget that reads "Detached merge request pipeline <ID> <status> for <SHA>"', () => {
+ pipeline.details.name = 'Detached merge request pipeline';
+ pipeline.merge_request_event_type = 'detached';
- factory();
+ factory();
- const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
- const actual = trimText(findPipelineInfoContainer().text());
+ const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
+ const actual = trimText(findPipelineInfoContainer().text());
- expect(actual).toBe(expected);
- });
+ expect(actual).toBe(expected);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/review_app_link_spec.js b/spec/frontend/vue_mr_widget/components/review_app_link_spec.js
index 7b063653a93..7d47621c64a 100644
--- a/spec/frontend/vue_mr_widget/components/review_app_link_spec.js
+++ b/spec/frontend/vue_mr_widget/components/review_app_link_spec.js
@@ -30,7 +30,7 @@ describe('review app link', () => {
});
it('renders provided cssClass as class attribute', () => {
- expect(el.getAttribute('class')).toEqual(props.cssClass);
+ expect(el.getAttribute('class')).toContain(props.cssClass);
});
it('renders View app text', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
index 67746b062b9..62fc3330444 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
@@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
-import Icon from '~/vue_shared/components/icon.vue';
describe('Commits header component', () => {
let wrapper;
@@ -23,7 +22,6 @@ describe('Commits header component', () => {
const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count');
const findCommitToggle = () => wrapper.find('.commit-edit-toggle');
- const findIcon = () => wrapper.find(Icon);
const findCommitsCountMessage = () => wrapper.find('.commits-count-message');
const findTargetBranchMessage = () => wrapper.find('.label-branch');
const findModifyButton = () => wrapper.find('.modify-message-button');
@@ -61,7 +59,7 @@ describe('Commits header component', () => {
wrapper.setData({ expanded: false });
return wrapper.vm.$nextTick().then(() => {
- expect(findIcon().props('name')).toBe('chevron-right');
+ expect(findCommitToggle().props('icon')).toBe('chevron-right');
});
});
@@ -119,7 +117,7 @@ describe('Commits header component', () => {
it('has a chevron-down icon', done => {
wrapper.vm.$nextTick(() => {
- expect(findIcon().props('name')).toBe('chevron-down');
+ expect(findCommitToggle().props('icon')).toBe('chevron-down');
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index c3a16a776a7..19f8a67d066 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -148,8 +148,8 @@ describe('MRWidgetConflicts', () => {
},
});
- expect(vm.contains('.js-resolve-conflicts-button')).toBe(false);
- expect(vm.contains('.js-merge-locally-button')).toBe(false);
+ expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false);
+ expect(vm.find('.js-merge-locally-button').exists()).toBe(false);
});
it('should not have resolve button when no conflict resolution path', () => {
@@ -161,7 +161,7 @@ describe('MRWidgetConflicts', () => {
},
});
- expect(vm.contains('.js-resolve-conflicts-button')).toBe(false);
+ expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index f591393d721..6778a8f4a1f 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -125,7 +125,11 @@ describe('MRWidgetFailedToMerge', () => {
});
it('renders refresh button', () => {
- expect(vm.$el.querySelector('.js-refresh-button').textContent.trim()).toEqual('Refresh now');
+ expect(
+ vm.$el
+ .querySelector('[data-testid="merge-request-failed-refresh-button"]')
+ .textContent.trim(),
+ ).toEqual('Refresh now');
});
it('renders remaining time', () => {
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index ffcf9b1477a..7fe6b44ecc7 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
@@ -49,8 +49,8 @@ describe('MrWidgetTerraformConainer', () => {
});
it('diplays loading skeleton', () => {
- expect(wrapper.contains(GlSkeletonLoading)).toBe(true);
- expect(wrapper.contains(MrWidgetExpanableSection)).toBe(false);
+ expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(false);
});
});
@@ -61,8 +61,8 @@ describe('MrWidgetTerraformConainer', () => {
});
it('displays terraform content', () => {
- expect(wrapper.contains(GlSkeletonLoading)).toBe(false);
- expect(wrapper.contains(MrWidgetExpanableSection)).toBe(true);
+ expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(true);
expect(findPlans()).toEqual(Object.values(plans));
});
@@ -156,7 +156,7 @@ describe('MrWidgetTerraformConainer', () => {
});
it('stops loading', () => {
- expect(wrapper.contains(GlSkeletonLoading)).toBe(false);
+ expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
});
it('generates one broken plan', () => {
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js
index 6adf4975414..bc0d2501809 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlIcon, GlLoadingIcon, GlDeprecatedButton } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlButton } from '@gitlab/ui';
import DeploymentActionButton from '~/vue_merge_request_widget/components/deployment/deployment_action_button.vue';
import {
CREATED,
@@ -13,6 +13,7 @@ const baseProps = {
actionsConfiguration: actionButtonMocks[DEPLOYING],
actionInProgress: null,
computedDeploymentStatus: CREATED,
+ icon: 'play',
};
describe('Deployment action button', () => {
@@ -28,18 +29,18 @@ describe('Deployment action button', () => {
wrapper.destroy();
});
- describe('when passed only icon', () => {
+ describe('when passed only icon via props', () => {
beforeEach(() => {
factory({
propsData: baseProps,
- slots: { default: ['<gl-icon name="stop" />'] },
+ slots: {},
stubs: {
'gl-icon': GlIcon,
},
});
});
- it('renders slot correctly', () => {
+ it('renders prop icon correctly', () => {
expect(wrapper.find(GlIcon).exists()).toBe(true);
});
});
@@ -49,7 +50,7 @@ describe('Deployment action button', () => {
factory({
propsData: baseProps,
slots: {
- default: ['<gl-icon name="play" />', `<span>${actionButtonMocks[DEPLOYING]}</span>`],
+ default: [`<span>${actionButtonMocks[DEPLOYING]}</span>`],
},
stubs: {
'gl-icon': GlIcon,
@@ -57,7 +58,7 @@ describe('Deployment action button', () => {
});
});
- it('renders slot correctly', () => {
+ it('renders slot and icon prop correctly', () => {
expect(wrapper.find(GlIcon).exists()).toBe(true);
expect(wrapper.text()).toContain(actionButtonMocks[DEPLOYING]);
});
@@ -75,7 +76,7 @@ describe('Deployment action button', () => {
it('is disabled and shows the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.find(GlDeprecatedButton).props('disabled')).toBe(true);
+ expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
@@ -90,7 +91,7 @@ describe('Deployment action button', () => {
});
it('is disabled and does not show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlDeprecatedButton).props('disabled')).toBe(true);
+ expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
@@ -106,7 +107,7 @@ describe('Deployment action button', () => {
});
it('is disabled and does not show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlDeprecatedButton).props('disabled')).toBe(true);
+ expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
});
@@ -118,7 +119,7 @@ describe('Deployment action button', () => {
});
it('is not disabled nor does it show the loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(GlDeprecatedButton).props('disabled')).toBe(false);
+ expect(wrapper.find(GlButton).props('disabled')).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index d64a7f88b6b..4688af30269 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -193,6 +193,7 @@ export default {
updated_at: '2017-04-07T15:28:44.800Z',
},
pipelineCoverageDelta: '15.25',
+ buildsWithCoverage: [{ name: 'karma', coverage: '40.2' }, { name: 'rspec', coverage: '80.4' }],
work_in_progress: false,
source_branch_exists: false,
mergeable_discussions_state: true,
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 0bbe040d031..a2ade44b7c4 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -530,7 +530,7 @@ describe('mrWidgetOptions', () => {
vm.mr.state = 'readyToMerge';
vm.$nextTick(() => {
- const tooltip = vm.$el.querySelector('.fa-question-circle');
+ const tooltip = vm.$el.querySelector('[data-testid="question-o-icon"]');
expect(vm.$el.textContent).toContain('Deletes source branch');
expect(tooltip.getAttribute('data-original-title')).toBe(
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index 128e0f39c41..631d4647b17 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -30,6 +30,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('notAllowedToMerge');
context.autoMergeEnabled = true;
+ context.hasMergeableDiscussionsState = true;
expect(bound()).toEqual('autoMergeEnabled');
@@ -44,6 +45,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('pipelineBlocked');
context.hasMergeableDiscussionsState = true;
+ context.autoMergeEnabled = false;
expect(bound()).toEqual('unresolvedDiscussions');
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
index 48326eda404..b691a366a0f 100644
--- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -69,6 +69,38 @@ describe('MergeRequestStore', () => {
});
});
+ describe('isPipelineBlocked', () => {
+ const pipelineWaitingForManualAction = {
+ details: {
+ status: {
+ group: 'manual',
+ },
+ },
+ };
+
+ it('should be `false` when the pipeline status is missing', () => {
+ store.setData({ ...mockData, pipeline: undefined });
+
+ expect(store.isPipelineBlocked).toBe(false);
+ });
+
+ it('should be `false` when the pipeline is waiting for manual action', () => {
+ store.setData({ ...mockData, pipeline: pipelineWaitingForManualAction });
+
+ expect(store.isPipelineBlocked).toBe(false);
+ });
+
+ it('should be `true` when the pipeline is waiting for manual action and the pipeline must succeed', () => {
+ store.setData({
+ ...mockData,
+ pipeline: pipelineWaitingForManualAction,
+ only_allow_merge_if_pipeline_succeeds: true,
+ });
+
+ expect(store.isPipelineBlocked).toBe(true);
+ });
+ });
+
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index e84eb7789d3..dfd114a2d1c 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
-<gl-new-dropdown-stub
+<gl-dropdown-stub
category="primary"
headertext=""
right=""
@@ -12,9 +12,9 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<div
class="pb-2 mx-1"
>
- <gl-new-dropdown-header-stub>
+ <gl-dropdown-section-header-stub>
Clone with SSH
- </gl-new-dropdown-header-stub>
+ </gl-dropdown-section-header-stub>
<div
class="mx-3"
@@ -53,9 +53,9 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
</div>
</div>
- <gl-new-dropdown-header-stub>
+ <gl-dropdown-section-header-stub>
Clone with HTTP
- </gl-new-dropdown-header-stub>
+ </gl-dropdown-section-header-stub>
<div
class="mx-3"
@@ -94,5 +94,5 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
</div>
</div>
</div>
-</gl-new-dropdown-stub>
+</gl-dropdown-stub>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index cd4728baeaa..c2b97f1e7f9 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -4,7 +4,7 @@ exports[`Expand button on click when short text is provided renders button after
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
style="display: none;"
type="button"
>
@@ -32,7 +32,7 @@ exports[`Expand button on click when short text is provided renders button after
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-append text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
style=""
type="button"
>
@@ -56,7 +56,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
type="button"
>
<!---->
@@ -83,7 +83,7 @@ exports[`Expand button when short text is provided renders button before text 1`
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
+ class="btn js-text-expander-append text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
style="display: none;"
type="button"
>
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
new file mode 100644
index 00000000000..4dde9d726d1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -0,0 +1,203 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlLink } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+
+const TEST_ACTION = {
+ key: 'action1',
+ text: 'Sample',
+ secondaryText: 'Lorem ipsum.',
+ tooltip: '',
+ href: '/sample',
+ attrs: { 'data-test': '123' },
+};
+const TEST_ACTION_2 = {
+ key: 'action2',
+ text: 'Sample 2',
+ secondaryText: 'Dolar sit amit.',
+ tooltip: 'Dolar sit amit.',
+ href: '#',
+ attrs: { 'data-test': '456' },
+};
+const TEST_TOOLTIP = 'Lorem ipsum dolar sit';
+
+describe('Actions button component', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = shallowMount(ActionsButton, {
+ propsData: { ...props },
+ directives: { GlTooltip: createMockDirective() },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getTooltip = child => {
+ const directiveBinding = getBinding(child.element, 'gl-tooltip');
+
+ return directiveBinding.value;
+ };
+ const findLink = () => wrapper.find(GlLink);
+ const findLinkTooltip = () => getTooltip(findLink());
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownTooltip = () => getTooltip(findDropdown());
+ const parseDropdownItems = () =>
+ findDropdown()
+ .findAll('gl-dropdown-item-stub,gl-dropdown-divider-stub')
+ .wrappers.map(x => {
+ if (x.is('gl-dropdown-divider-stub')) {
+ return { type: 'divider' };
+ }
+
+ const { isCheckItem, isChecked, secondaryText } = x.props();
+
+ return {
+ type: 'item',
+ isCheckItem,
+ isChecked,
+ secondaryText,
+ text: x.text(),
+ };
+ });
+ const clickOn = (child, evt = new Event('click')) => child.vm.$emit('click', evt);
+ const clickLink = (...args) => clickOn(findLink(), ...args);
+ const clickDropdown = (...args) => clickOn(findDropdown(), ...args);
+
+ describe('with 1 action', () => {
+ beforeEach(() => {
+ createComponent({ actions: [TEST_ACTION] });
+ });
+
+ it('should not render dropdown', () => {
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('should render single button', () => {
+ const link = findLink();
+
+ expect(link.attributes()).toEqual({
+ class: expect.any(String),
+ href: TEST_ACTION.href,
+ ...TEST_ACTION.attrs,
+ });
+ expect(link.text()).toBe(TEST_ACTION.text);
+ });
+
+ it('should have tooltip', () => {
+ expect(findLinkTooltip()).toBe(TEST_ACTION.tooltip);
+ });
+
+ it('should have attrs', () => {
+ expect(findLink().attributes()).toMatchObject(TEST_ACTION.attrs);
+ });
+
+ it('can click', () => {
+ expect(clickLink).not.toThrow();
+ });
+ });
+
+ describe('with 1 action with tooltip', () => {
+ it('should have tooltip', () => {
+ createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] });
+
+ expect(findLinkTooltip()).toBe(TEST_TOOLTIP);
+ });
+ });
+
+ describe('with 1 action with handle', () => {
+ it('can click and trigger handle', () => {
+ const handleClick = jest.fn();
+ createComponent({ actions: [{ ...TEST_ACTION, handle: handleClick }] });
+
+ const event = new Event('click');
+ clickLink(event);
+
+ expect(handleClick).toHaveBeenCalledWith(event);
+ });
+ });
+
+ describe('with multiple actions', () => {
+ let handleAction;
+
+ beforeEach(() => {
+ handleAction = jest.fn();
+
+ createComponent({ actions: [{ ...TEST_ACTION, handle: handleAction }, TEST_ACTION_2] });
+ });
+
+ it('should default to selecting first action', () => {
+ expect(findDropdown().attributes()).toMatchObject({
+ text: TEST_ACTION.text,
+ 'split-href': TEST_ACTION.href,
+ });
+ });
+
+ it('should handle first action click', () => {
+ const event = new Event('click');
+
+ clickDropdown(event);
+
+ expect(handleAction).toHaveBeenCalledWith(event);
+ });
+
+ it('should render dropdown items', () => {
+ expect(parseDropdownItems()).toEqual([
+ {
+ type: 'item',
+ isCheckItem: true,
+ isChecked: true,
+ secondaryText: TEST_ACTION.secondaryText,
+ text: TEST_ACTION.text,
+ },
+ { type: 'divider' },
+ {
+ type: 'item',
+ isCheckItem: true,
+ isChecked: false,
+ secondaryText: TEST_ACTION_2.secondaryText,
+ text: TEST_ACTION_2.text,
+ },
+ ]);
+ });
+
+ it('should select action 2 when clicked', () => {
+ expect(wrapper.emitted('select')).toBeUndefined();
+
+ const action2 = wrapper.find(`[data-testid="action_${TEST_ACTION_2.key}"]`);
+ action2.vm.$emit('click');
+
+ expect(wrapper.emitted('select')).toEqual([[TEST_ACTION_2.key]]);
+ });
+
+ it('should have tooltip value', () => {
+ expect(findDropdownTooltip()).toBe(TEST_ACTION.tooltip);
+ });
+ });
+
+ describe('with multiple actions and selectedKey', () => {
+ beforeEach(() => {
+ createComponent({ actions: [TEST_ACTION, TEST_ACTION_2], selectedKey: TEST_ACTION_2.key });
+ });
+
+ it('should show action 2 as selected', () => {
+ expect(parseDropdownItems()).toEqual([
+ expect.objectContaining({
+ type: 'item',
+ isChecked: false,
+ }),
+ { type: 'divider' },
+ expect.objectContaining({
+ type: 'item',
+ isChecked: true,
+ }),
+ ]);
+ });
+
+ it('should have tooltip value', () => {
+ expect(findDropdownTooltip()).toBe(TEST_ACTION_2.tooltip);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/alert_detail_table_spec.js b/spec/frontend/vue_shared/components/alert_detail_table_spec.js
new file mode 100644
index 00000000000..9c38ccad8a7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/alert_detail_table_spec.js
@@ -0,0 +1,74 @@
+import { mount } from '@vue/test-utils';
+import { GlTable, GlLoadingIcon } from '@gitlab/ui';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+
+const mockAlert = {
+ iid: '1527542',
+ title: 'SyntaxError: Invalid or unexpected token',
+ severity: 'CRITICAL',
+ eventCount: 7,
+ createdAt: '2020-04-17T23:18:14.996Z',
+ startedAt: '2020-04-17T23:18:14.996Z',
+ endedAt: '2020-04-17T23:18:14.996Z',
+ status: 'TRIGGERED',
+ assignees: { nodes: [] },
+ notes: { nodes: [] },
+ todos: { nodes: [] },
+};
+
+describe('AlertDetails', () => {
+ let wrapper;
+
+ function mountComponent(propsData = {}) {
+ wrapper = mount(AlertDetailsTable, {
+ propsData: {
+ alert: mockAlert,
+ loading: false,
+ ...propsData,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTableComponent = () => wrapper.find(GlTable);
+
+ describe('Alert details', () => {
+ describe('empty state', () => {
+ beforeEach(() => {
+ mountComponent({ alert: null });
+ });
+
+ it('shows an empty state when no alert is provided', () => {
+ expect(wrapper.text()).toContain('No alert data to display.');
+ });
+ });
+
+ describe('loading state', () => {
+ beforeEach(() => {
+ mountComponent({ loading: true });
+ });
+
+ it('displays a loading state when loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('with table data', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders a table', () => {
+ expect(findTableComponent().exists()).toBe(true);
+ });
+
+ it('renders a cell based on alert data', () => {
+ expect(findTableComponent().text()).toContain('SyntaxError: Invalid or unexpected token');
+ });
+ });
+ });
+});
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 5cf42ecdc1d..22643a17b2b 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
@@ -36,6 +36,6 @@ describe('Blob Rich Viewer component', () => {
});
it('is using Markdown View Field', () => {
- expect(wrapper.contains(MarkdownFieldView)).toBe(true);
+ expect(wrapper.find(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 03519a6f803..80918c5e771 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const changedFile = () => ({ changed: true });
const stagedFile = () => ({ changed: true, staged: true });
@@ -25,7 +25,7 @@ describe('Changed file icon', () => {
wrapper.destroy();
});
- const findIcon = () => wrapper.find(Icon);
+ const findIcon = () => wrapper.find(GlIcon);
const findIconName = () => findIcon().props('name');
const findIconClasses = () => findIcon().classes();
const findTooltipText = () => wrapper.attributes('title');
diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
index d9829874b93..5b8576ad761 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlFormInputGroup, GlNewDropdownHeader } from '@gitlab/ui';
+import { GlFormInputGroup, GlDropdownSectionHeader } from '@gitlab/ui';
import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue';
describe('Clone Dropdown Button', () => {
@@ -40,7 +40,7 @@ describe('Clone Dropdown Button', () => {
createComponent();
const group = wrapper.findAll(GlFormInputGroup).at(index);
expect(group.props('value')).toBe(value);
- expect(group.contains(GlFormInputGroup)).toBe(true);
+ expect(group.find(GlFormInputGroup).exists()).toBe(true);
});
it.each`
@@ -51,7 +51,7 @@ describe('Clone Dropdown Button', () => {
createComponent({ [name]: value });
expect(wrapper.find(GlFormInputGroup).props('value')).toBe(value);
- expect(wrapper.findAll(GlNewDropdownHeader).length).toBe(1);
+ expect(wrapper.findAll(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(GlNewDropdownHeader).length).toBe(1);
+ expect(wrapper.findAll(GlDropdownSectionHeader).length).toBe(1);
});
it('correctly calculates httpLabel for HTTPS protocol', () => {
createComponent({ httpLink: httpsLink });
- expect(wrapper.find(GlNewDropdownHeader).text()).toContain('HTTPS');
+ expect(wrapper.find(GlDropdownSectionHeader).text()).toContain('HTTPS');
});
});
});
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 3510c9b699d..9b5c0941a0d 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import CommitComponent from '~/vue_shared/components/commit.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('Commit component', () => {
@@ -8,7 +8,7 @@ describe('Commit component', () => {
let wrapper;
const findIcon = name => {
- const icons = wrapper.findAll(Icon).filter(c => c.attributes('name') === name);
+ const icons = wrapper.findAll(GlIcon).filter(c => c.attributes('name') === name);
return icons.length ? icons.at(0) : icons;
};
@@ -46,7 +46,7 @@ describe('Commit component', () => {
expect(
wrapper
.find('.icon-container')
- .find(Icon)
+ .find(GlIcon)
.exists(),
).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js
index 7bccd6f1a64..5d92af64de0 100644
--- a/spec/frontend/vue_shared/components/confirm_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
@@ -21,9 +20,14 @@ describe('vue_shared/components/confirm_modal', () => {
selector: '.test-button',
};
- const actionSpies = {
- openModal: jest.fn(),
- closeModal: jest.fn(),
+ const popupMethods = {
+ hide: jest.fn(),
+ show: jest.fn(),
+ };
+
+ const GlModalStub = {
+ template: '<div><slot></slot></div>',
+ methods: popupMethods,
};
let wrapper;
@@ -34,8 +38,8 @@ describe('vue_shared/components/confirm_modal', () => {
...defaultProps,
...props,
},
- methods: {
- ...actionSpies,
+ stubs: {
+ GlModal: GlModalStub,
},
});
};
@@ -44,7 +48,7 @@ describe('vue_shared/components/confirm_modal', () => {
wrapper.destroy();
});
- const findModal = () => wrapper.find(GlModal);
+ const findModal = () => wrapper.find(GlModalStub);
const findForm = () => wrapper.find('form');
const findFormData = () =>
findForm()
@@ -103,7 +107,7 @@ describe('vue_shared/components/confirm_modal', () => {
});
it('does not close modal', () => {
- expect(actionSpies.closeModal).not.toHaveBeenCalled();
+ expect(popupMethods.hide).not.toHaveBeenCalled();
});
describe('when modal closed', () => {
@@ -112,7 +116,7 @@ describe('vue_shared/components/confirm_modal', () => {
});
it('closes modal', () => {
- expect(actionSpies.closeModal).toHaveBeenCalled();
+ expect(popupMethods.hide).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index 223e22d650b..afd1f1a3123 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -234,7 +234,8 @@ describe('DateTimePicker', () => {
});
it('unchecks quick range when text is input is clicked', () => {
- const findActiveItems = () => findQuickRangeItems().filter(w => w.is('.active'));
+ const findActiveItems = () =>
+ findQuickRangeItems().filter(w => w.classes().includes('active'));
expect(findActiveItems().length).toBe(1);
@@ -332,13 +333,13 @@ describe('DateTimePicker', () => {
expect(items.length).toBe(Object.keys(otherTimeRanges).length);
expect(items.at(0).text()).toBe('1 minute');
- expect(items.at(0).is('.active')).toBe(false);
+ expect(items.at(0).classes()).not.toContain('active');
expect(items.at(1).text()).toBe('2 minutes');
- expect(items.at(1).is('.active')).toBe(true);
+ expect(items.at(1).classes()).toContain('active');
expect(items.at(2).text()).toBe('5 minutes');
- expect(items.at(2).is('.active')).toBe(false);
+ expect(items.at(2).classes()).not.toContain('active');
});
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
index e0e982f4e11..e91e6577aaf 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
@@ -14,19 +14,13 @@ import {
const localVue = createLocalVue();
localVue.use(Vuex);
-function createRenamedComponent({
- props = {},
- methods = {},
- store = new Vuex.Store({}),
- deep = false,
-}) {
+function createRenamedComponent({ props = {}, store = new Vuex.Store({}), deep = false }) {
const mnt = deep ? mount : shallowMount;
return mnt(Renamed, {
propsData: { ...props },
localVue,
store,
- methods,
});
}
@@ -258,25 +252,17 @@ describe('Renamed Diff Viewer', () => {
'includes a link to the full file for alternate viewer type "$altType"',
({ altType, linkText }) => {
const file = { ...diffFile };
- const clickMock = jest.fn().mockImplementation(() => {});
file.alternate_viewer.name = altType;
wrapper = createRenamedComponent({
deep: true,
props: { diffFile: file },
- methods: {
- clickLink: clickMock,
- },
});
const link = wrapper.find('a');
expect(link.text()).toEqual(linkText);
expect(link.attributes('href')).toEqual(DIFF_FILE_VIEW_PATH);
-
- link.vm.$emit('click');
-
- expect(clickMock).toHaveBeenCalled();
},
);
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
index ffdeb25439c..efa30bf6605 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
@@ -32,12 +32,6 @@ describe('DropdownSearchInputComponent', () => {
expect(wrapper.find('.fa-search.dropdown-input-search').exists()).toBe(true);
});
- it('renders clear search icon element', () => {
- expect(wrapper.find('.fa-times.dropdown-input-clear.js-dropdown-input-clear').exists()).toBe(
- true,
- );
- });
-
it('displays custom placeholder text', () => {
expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText);
});
diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js
index f9e56774526..40026021777 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -84,7 +84,7 @@ describe('File finder item spec', () => {
waitForPromises()
.then(() => {
- vm.$el.querySelector('.dropdown-input-clear').click();
+ vm.clearSearchInput();
})
.then(waitForPromises)
.then(() => {
@@ -94,13 +94,13 @@ describe('File finder item spec', () => {
.catch(done.fail);
});
- it('clear button focues search input', done => {
+ it('clear button focuses search input', done => {
jest.spyOn(vm.$refs.searchInput, 'focus').mockImplementation(() => {});
vm.searchText = 'index';
waitForPromises()
.then(() => {
- vm.$el.querySelector('.dropdown-input-clear').click();
+ vm.clearSearchInput();
})
.then(waitForPromises)
.then(() => {
@@ -319,8 +319,8 @@ describe('File finder item spec', () => {
.catch(done.fail);
});
- it('calls toggle on `command+p` key press', done => {
- Mousetrap.trigger('command+p');
+ it('calls toggle on `mod+p` key press', done => {
+ Mousetrap.trigger('mod+p');
vm.$nextTick()
.then(() => {
@@ -330,39 +330,28 @@ describe('File finder item spec', () => {
.catch(done.fail);
});
- it('calls toggle on `ctrl+p` key press', done => {
- Mousetrap.trigger('ctrl+p');
-
- vm.$nextTick()
- .then(() => {
- expect(vm.toggle).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('always allows `command+p` to trigger toggle', () => {
+ it('always allows `mod+p` to trigger toggle', () => {
expect(
- vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
- ).toBe(false);
- });
-
- it('always allows `ctrl+p` to trigger toggle', () => {
- expect(
- vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
+ Mousetrap.prototype.stopCallback(
+ null,
+ vm.$el.querySelector('.dropdown-input-field'),
+ 'mod+p',
+ ),
).toBe(false);
});
it('onlys handles `t` when focused in input-field', () => {
expect(
- vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
+ Mousetrap.prototype.stopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
).toBe(true);
});
it('stops callback in monaco editor', () => {
setFixtures('<div class="inputarea"></div>');
- expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
+ expect(
+ Mousetrap.prototype.stopCallback(null, document.querySelector('.inputarea'), 't'),
+ ).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index 1acd2e05464..d28c35d26bf 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -118,7 +118,7 @@ describe('File row component', () => {
level: 0,
});
- expect(wrapper.contains(FileHeader)).toBe(true);
+ expect(wrapper.find(FileHeader).exists()).toBe(true);
});
it('matches the current route against encoded file URL', () => {
@@ -139,4 +139,16 @@ describe('File row component', () => {
expect(wrapper.vm.hasUrlAtCurrentRoute()).toBe(true);
});
+
+ it('render with the correct file classes prop', () => {
+ createComponent({
+ file: {
+ ...file(),
+ },
+ level: 0,
+ fileClasses: 'font-weight-bold',
+ });
+
+ expect(wrapper.find('.file-row-name').classes()).toContain('font-weight-bold');
+ });
});
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 73dbecadd89..c79880d4766 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
@@ -1,19 +1,28 @@
import { shallowMount, mount } from '@vue/test-utils';
-import {
- GlFilteredSearch,
- GlButtonGroup,
- GlButton,
- GlNewDropdown as GlDropdown,
- GlNewDropdownItem as GlDropdownItem,
-} from '@gitlab/ui';
+import { GlFilteredSearch, GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import { mockAvailableTokens, mockSortOptions, mockHistoryItems } from './mock_data';
+import {
+ mockAvailableTokens,
+ mockSortOptions,
+ mockHistoryItems,
+ tokenValueAuthor,
+ tokenValueLabel,
+ tokenValueMilestone,
+} from './mock_data';
+
+jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
+ uniqueTokens: jest.fn().mockImplementation(tokens => tokens),
+ stripQuotes: jest.requireActual(
+ '~/vue_shared/components/filtered_search_bar/filtered_search_utils',
+ ).stripQuotes,
+}));
const createComponent = ({
shallow = true,
@@ -52,10 +61,10 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapper.vm.filterValue).toEqual([]);
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending);
expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
- expect(wrapper.contains(GlButtonGroup)).toBe(true);
- expect(wrapper.contains(GlButton)).toBe(true);
- expect(wrapper.contains(GlDropdown)).toBe(true);
- expect(wrapper.contains(GlDropdownItem)).toBe(true);
+ 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);
});
it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => {
@@ -63,23 +72,31 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapperNoSort.vm.filterValue).toEqual([]);
expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined);
- expect(wrapperNoSort.contains(GlButtonGroup)).toBe(false);
- expect(wrapperNoSort.contains(GlButton)).toBe(false);
- expect(wrapperNoSort.contains(GlDropdown)).toBe(false);
- expect(wrapperNoSort.contains(GlDropdownItem)).toBe(false);
+ 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);
});
});
describe('computed', () => {
describe('tokenSymbols', () => {
it('returns a map containing type and symbols from `tokens` prop', () => {
- expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@', label_name: '~' });
+ expect(wrapper.vm.tokenSymbols).toEqual({
+ author_username: '@',
+ label_name: '~',
+ milestone_title: '%',
+ });
});
});
describe('tokenTitles', () => {
it('returns a map containing type and title from `tokens` prop', () => {
- expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author', label_name: 'Label' });
+ expect(wrapper.vm.tokenTitles).toEqual({
+ author_username: 'Author',
+ label_name: 'Label',
+ milestone_title: 'Milestone',
+ });
});
});
@@ -131,6 +148,20 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapper.vm.filteredRecentSearches[0]).toEqual({ foo: 'bar' });
});
+ it('returns array of recent searches sanitizing any duplicate token values', async () => {
+ wrapper.setData({
+ recentSearches: [
+ [tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValueLabel],
+ [tokenValueAuthor, tokenValueMilestone],
+ ],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.filteredRecentSearches).toHaveLength(2);
+ expect(uniqueTokens).toHaveBeenCalled();
+ });
+
it('returns undefined when recentSearchesStorageKey prop is not set on component', async () => {
wrapper.setProps({
recentSearchesStorageKey: '',
@@ -182,40 +213,12 @@ describe('FilteredSearchBarRoot', () => {
});
describe('removeQuotesEnclosure', () => {
- const mockFilters = [
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- {
- type: 'label_name',
- value: {
- data: '"Documentation Update"',
- operator: '=',
- },
- },
- 'foo',
- ];
+ const mockFilters = [tokenValueAuthor, tokenValueLabel, 'foo'];
it('returns filter array with unescaped strings for values which have spaces', () => {
expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- {
- type: 'label_name',
- value: {
- data: 'Documentation Update',
- operator: '=',
- },
- },
+ tokenValueAuthor,
+ tokenValueLabel,
'foo',
]);
});
@@ -277,21 +280,26 @@ describe('FilteredSearchBarRoot', () => {
});
describe('handleFilterSubmit', () => {
- const mockFilters = [
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- 'foo',
- ];
+ const mockFilters = [tokenValueAuthor, 'foo'];
+
+ beforeEach(async () => {
+ wrapper.setData({
+ filterValue: mockFilters,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('calls `uniqueTokens` on `filterValue` prop to remove duplicates', () => {
+ wrapper.vm.handleFilterSubmit();
+
+ expect(uniqueTokens).toHaveBeenCalledWith(wrapper.vm.filterValue);
+ });
it('calls `recentSearchesStore.addRecentSearch` with serialized value of provided `filters` param', () => {
jest.spyOn(wrapper.vm.recentSearchesStore, 'addRecentSearch');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesStore.addRecentSearch).toHaveBeenCalledWith(mockFilters);
@@ -301,7 +309,7 @@ describe('FilteredSearchBarRoot', () => {
it('calls `recentSearchesService.save` with array of searches', () => {
jest.spyOn(wrapper.vm.recentSearchesService, 'save');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([mockFilters]);
@@ -311,7 +319,7 @@ describe('FilteredSearchBarRoot', () => {
it('sets `recentSearches` data prop with array of searches', () => {
jest.spyOn(wrapper.vm.recentSearchesService, 'save');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
return wrapper.vm.recentSearchesPromise.then(() => {
expect(wrapper.vm.recentSearches).toEqual([mockFilters]);
@@ -329,7 +337,7 @@ describe('FilteredSearchBarRoot', () => {
it('emits component event `onFilter` with provided filters param', () => {
jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
- wrapper.vm.handleFilterSubmit(mockFilters);
+ wrapper.vm.handleFilterSubmit();
expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]);
expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockFilters);
@@ -366,7 +374,9 @@ describe('FilteredSearchBarRoot', () => {
'.gl-search-box-by-click-menu .gl-search-box-by-click-history-item',
);
- expect(searchHistoryItemsEl.at(0).text()).toBe('Author := @tobyLabel := ~Bug"duo"');
+ expect(searchHistoryItemsEl.at(0).text()).toBe(
+ 'Author := @rootLabel := ~bugMilestone := %v1.0"duo"',
+ );
wrapperFullMount.destroy();
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
index a857f84adf1..4869e75a2f3 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -1,4 +1,18 @@
-import * as filteredSearchUtils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+import {
+ stripQuotes,
+ uniqueTokens,
+ prepareTokens,
+ processFilters,
+ filterToQueryObject,
+ urlQueryToFilter,
+} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+
+import {
+ tokenValueAuthor,
+ tokenValueLabel,
+ tokenValueMilestone,
+ tokenValuePlain,
+} from './mock_data';
describe('Filtered Search Utils', () => {
describe('stripQuotes', () => {
@@ -9,11 +23,196 @@ describe('Filtered Search Utils', () => {
${'FooBar'} | ${'FooBar'}
${"Foo'Bar"} | ${"Foo'Bar"}
${'Foo"Bar'} | ${'Foo"Bar'}
+ ${'Foo Bar'} | ${'Foo Bar'}
`(
'returns string $outputValue when called with string $inputValue',
({ inputValue, outputValue }) => {
- expect(filteredSearchUtils.stripQuotes(inputValue)).toBe(outputValue);
+ expect(stripQuotes(inputValue)).toBe(outputValue);
},
);
});
+
+ describe('uniqueTokens', () => {
+ it('returns tokens array with duplicates removed', () => {
+ expect(
+ uniqueTokens([
+ tokenValueAuthor,
+ tokenValueLabel,
+ tokenValueMilestone,
+ tokenValueLabel,
+ tokenValuePlain,
+ ]),
+ ).toHaveLength(4); // Removes 2nd instance of tokenValueLabel
+ });
+
+ it('returns tokens array as it is if it does not have duplicates', () => {
+ expect(
+ uniqueTokens([tokenValueAuthor, tokenValueLabel, tokenValueMilestone, tokenValuePlain]),
+ ).toHaveLength(4);
+ });
+ });
+});
+
+describe('prepareTokens', () => {
+ describe('with empty data', () => {
+ it('returns an empty array', () => {
+ expect(prepareTokens()).toEqual([]);
+ expect(prepareTokens({})).toEqual([]);
+ expect(prepareTokens({ milestone: null, author: null, assignees: [], labels: [] })).toEqual(
+ [],
+ );
+ });
+ });
+
+ it.each([
+ [
+ 'milestone',
+ { value: 'v1.0', operator: '=' },
+ [{ type: 'milestone', value: { data: 'v1.0', operator: '=' } }],
+ ],
+ [
+ 'author',
+ { value: 'mr.popo', operator: '!=' },
+ [{ type: 'author', value: { data: 'mr.popo', operator: '!=' } }],
+ ],
+ [
+ 'labels',
+ [{ value: 'z-fighters', operator: '=' }],
+ [{ type: 'labels', value: { data: 'z-fighters', operator: '=' } }],
+ ],
+ [
+ 'assignees',
+ [{ value: 'krillin', operator: '=' }, { value: 'piccolo', operator: '!=' }],
+ [
+ { type: 'assignees', value: { data: 'krillin', operator: '=' } },
+ { type: 'assignees', value: { data: 'piccolo', operator: '!=' } },
+ ],
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }],
+ [
+ { type: 'foo', value: { data: 'bar', operator: '!=' } },
+ { type: 'foo', value: { data: 'baz', operator: '!=' } },
+ ],
+ ],
+ ])('gathers %s=%j into result=%j', (token, value, result) => {
+ const res = prepareTokens({ [token]: value });
+ expect(res).toEqual(result);
+ });
+});
+
+describe('processFilters', () => {
+ it('processes multiple filter values', () => {
+ const result = processFilters([
+ { type: 'foo', value: { data: 'foo', operator: '=' } },
+ { type: 'bar', value: { data: 'bar1', operator: '=' } },
+ { type: 'bar', value: { data: 'bar2', operator: '!=' } },
+ ]);
+
+ expect(result).toStrictEqual({
+ foo: [{ value: 'foo', operator: '=' }],
+ bar: [{ value: 'bar1', operator: '=' }, { value: 'bar2', operator: '!=' }],
+ });
+ });
+
+ it('does not remove wrapping double quotes from the data', () => {
+ const result = processFilters([
+ { type: 'foo', value: { data: '"value with spaces"', operator: '=' } },
+ ]);
+
+ expect(result).toStrictEqual({
+ foo: [{ value: '"value with spaces"', operator: '=' }],
+ });
+ });
+});
+
+describe('filterToQueryObject', () => {
+ describe('with empty data', () => {
+ it('returns an empty object', () => {
+ expect(filterToQueryObject()).toEqual({});
+ expect(filterToQueryObject({})).toEqual({});
+ expect(filterToQueryObject({ author_username: null, label_name: [] })).toEqual({
+ author_username: null,
+ label_name: null,
+ 'not[author_username]': null,
+ 'not[label_name]': null,
+ });
+ });
+ });
+
+ it.each([
+ [
+ 'author_username',
+ { value: 'v1.0', operator: '=' },
+ { author_username: 'v1.0', 'not[author_username]': null },
+ ],
+ [
+ 'author_username',
+ { value: 'v1.0', operator: '!=' },
+ { author_username: null, 'not[author_username]': 'v1.0' },
+ ],
+ [
+ 'label_name',
+ [{ value: 'z-fighters', operator: '=' }],
+ { label_name: ['z-fighters'], 'not[label_name]': null },
+ ],
+ [
+ 'label_name',
+ [{ value: 'z-fighters', operator: '!=' }],
+ { label_name: null, 'not[label_name]': ['z-fighters'] },
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }],
+ { foo: ['bar', 'baz'], 'not[foo]': null },
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }],
+ { foo: null, 'not[foo]': ['bar', 'baz'] },
+ ],
+ [
+ 'foo',
+ [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '=' }],
+ { foo: ['baz'], 'not[foo]': ['bar'] },
+ ],
+ ])('gathers filter values %s=%j into query object=%j', (token, value, result) => {
+ const res = filterToQueryObject({ [token]: value });
+ expect(res).toEqual(result);
+ });
+});
+
+describe('urlQueryToFilter', () => {
+ describe('with empty data', () => {
+ it('returns an empty object', () => {
+ expect(urlQueryToFilter()).toEqual({});
+ expect(urlQueryToFilter('')).toEqual({});
+ expect(urlQueryToFilter('author_username=&milestone_title=&')).toEqual({});
+ });
+ });
+
+ it.each([
+ ['author_username=v1.0', { author_username: { value: 'v1.0', operator: '=' } }],
+ ['not[author_username]=v1.0', { author_username: { value: 'v1.0', operator: '!=' } }],
+ ['foo=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }],
+ ['foo=bar&foo[]=baz', { foo: [{ value: 'baz', operator: '=' }] }],
+ ['not[foo]=bar&foo=baz', { foo: { value: 'baz', operator: '=' } }],
+ [
+ 'foo[]=bar&foo[]=baz&not[foo]=',
+ { foo: [{ value: 'bar', operator: '=' }, { value: 'baz', operator: '=' }] },
+ ],
+ [
+ 'foo[]=&not[foo][]=bar&not[foo][]=baz',
+ { foo: [{ value: 'bar', operator: '!=' }, { value: 'baz', operator: '!=' }] },
+ ],
+ [
+ 'foo[]=baz&not[foo][]=bar',
+ { foo: [{ value: 'baz', operator: '=' }, { value: 'bar', operator: '!=' }] },
+ ],
+ ['not[foo][]=bar', { foo: [{ value: 'bar', operator: '!=' }] }],
+ ])('gathers filter values %s into query object=%j', (query, result) => {
+ const res = urlQueryToFilter(query);
+ expect(res).toEqual(result);
+ });
});
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 dcccb1f49b6..e0a3208cac9 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
@@ -1,6 +1,7 @@
import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import Api from '~/api';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -33,6 +34,8 @@ export const mockAuthor3 = {
export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
+export const mockBranches = [{ name: 'Master' }, { name: 'v1.x' }, { name: 'my-Branch' }];
+
export const mockRegularMilestone = {
id: 1,
name: '4.0',
@@ -55,6 +58,16 @@ export const mockMilestones = [
mockEscapedMilestone,
];
+export const mockBranchToken = {
+ type: 'source_branch',
+ icon: 'branch',
+ title: 'Source Branch',
+ unique: true,
+ token: BranchToken,
+ operators: [{ value: '=', description: 'is', default: 'true' }],
+ fetchBranches: Api.branches.bind(Api),
+};
+
export const mockAuthorToken = {
type: 'author_username',
icon: 'user',
@@ -89,36 +102,40 @@ export const mockMilestoneToken = {
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
-export const mockAvailableTokens = [mockAuthorToken, mockLabelToken];
+export const mockAvailableTokens = [mockAuthorToken, mockLabelToken, mockMilestoneToken];
+
+export const tokenValueAuthor = {
+ type: 'author_username',
+ value: {
+ data: 'root',
+ operator: '=',
+ },
+};
+
+export const tokenValueLabel = {
+ type: 'label_name',
+ value: {
+ operator: '=',
+ data: 'bug',
+ },
+};
+
+export const tokenValueMilestone = {
+ type: 'milestone_title',
+ value: {
+ operator: '=',
+ data: 'v1.0',
+ },
+};
+
+export const tokenValuePlain = {
+ type: 'filtered-search-term',
+ value: { data: 'foo' },
+};
export const mockHistoryItems = [
- [
- {
- type: 'author_username',
- value: {
- data: 'toby',
- operator: '=',
- },
- },
- {
- type: 'label_name',
- value: {
- data: 'Bug',
- operator: '=',
- },
- },
- 'duo',
- ],
- [
- {
- type: 'author_username',
- value: {
- data: 'root',
- operator: '=',
- },
- },
- 'si',
- ],
+ [tokenValueAuthor, tokenValueLabel, tokenValueMilestone, 'duo'],
+ [tokenValueAuthor, 'si'],
];
export const mockSortOptions = [
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 160febf9d06..72840ce381f 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
@@ -1,18 +1,42 @@
import { mount } from '@vue/test-utils';
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchTokenSegment,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import {
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { mockAuthorToken, mockAuthors } from '../mock_data';
jest.mock('~/flash');
-
-const createComponent = ({ config = mockAuthorToken, value = { data: '' }, active = false } = {}) =>
- mount(AuthorToken, {
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockAuthorToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(AuthorToken, {
propsData: {
config,
value,
@@ -22,18 +46,9 @@ const createComponent = ({ config = mockAuthorToken, value = { data: '' }, activ
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
- stubs: {
- Portal: {
- template: '<div><slot></slot></div>',
- },
- GlFilteredSearchSuggestionList: {
- template: '<div></div>',
- methods: {
- getValue: () => '=',
- },
- },
- },
+ stubs,
});
+}
describe('AuthorToken', () => {
let mock;
@@ -141,5 +156,57 @@ describe('AuthorToken', () => {
expect(tokenSegments.at(2).text()).toBe(mockAuthors[0].name); // "Administrator"
});
});
+
+ it('renders provided defaultAuthors as suggestions', async () => {
+ const defaultAuthors = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockAuthorToken, defaultAuthors },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultAuthors.length);
+ defaultAuthors.forEach((label, index) => {
+ expect(suggestions.at(index).text()).toBe(label.text);
+ });
+ });
+
+ it('does not render divider when no defaultAuthors', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockAuthorToken, defaultAuthors: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockAuthorToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(1);
+ expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_ANY.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
new file mode 100644
index 00000000000..12b7fd58670
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -0,0 +1,207 @@
+import { mount } from '@vue/test-utils';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import {
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
+
+import { mockBranches, mockBranchToken } from '../mock_data';
+
+jest.mock('~/flash');
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockBranchToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(BranchToken, {
+ propsData: {
+ config,
+ value,
+ active,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ },
+ stubs,
+ });
+}
+
+describe('BranchToken', () => {
+ let mock;
+ let wrapper;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({ value: { data: mockBranches[0].name } });
+
+ wrapper.setData({
+ branches: mockBranches,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ describe('currentValue', () => {
+ it('returns lowercase string for `value.data`', () => {
+ expect(wrapper.vm.currentValue).toBe('master');
+ });
+ });
+
+ describe('activeBranch', () => {
+ it('returns object for currently present `value.data`', () => {
+ expect(wrapper.vm.activeBranch).toEqual(mockBranches[0]);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('fetchBranchBySearchTerm', () => {
+ it('calls `config.fetchBranches` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches');
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo');
+ });
+
+ it('sets response to `branches` when request is succesful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches });
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.branches).toEqual(mockBranches);
+ });
+ });
+
+ it('calls `createFlash` with flash error message when request fails', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching branches.',
+ });
+ });
+ });
+
+ it('sets `loading` to false when request completes', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
+
+ wrapper.vm.fetchBranchBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.loading).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ const defaultBranches = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+ async function showSuggestions() {
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+ }
+
+ beforeEach(async () => {
+ wrapper = createComponent({ value: { data: mockBranches[0].name } });
+
+ wrapper.setData({
+ branches: mockBranches,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('renders gl-filtered-search-token component', () => {
+ expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ });
+
+ it('renders token item when value is selected', () => {
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3);
+ expect(tokenSegments.at(2).text()).toBe(mockBranches[0].name);
+ });
+
+ it('renders provided defaultBranches as suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockBranchToken, defaultBranches },
+ stubs: { Portal: true },
+ });
+ await showSuggestions();
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultBranches.length);
+ defaultBranches.forEach((branch, index) => {
+ expect(suggestions.at(index).text()).toBe(branch.text);
+ });
+ });
+
+ it('does not render divider when no defaultBranches', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockBranchToken, defaultBranches: [] },
+ stubs: { Portal: true },
+ });
+ await showSuggestions();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders no suggestions as default', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockBranchToken },
+ stubs: { Portal: true },
+ });
+ await showSuggestions();
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(0);
+ });
+ });
+});
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 0e60ee99327..3feb05bab35 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
@@ -1,5 +1,10 @@
import { mount } from '@vue/test-utils';
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import {
@@ -9,14 +14,34 @@ import {
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import {
+ DEFAULT_LABELS,
+ DEFAULT_LABEL_NONE,
+ DEFAULT_LABEL_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { mockLabelToken } from '../mock_data';
jest.mock('~/flash');
-
-const createComponent = ({ config = mockLabelToken, value = { data: '' }, active = false } = {}) =>
- mount(LabelToken, {
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockLabelToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(LabelToken, {
propsData: {
config,
value,
@@ -26,18 +51,9 @@ const createComponent = ({ config = mockLabelToken, value = { data: '' }, active
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
- stubs: {
- Portal: {
- template: '<div><slot></slot></div>',
- },
- GlFilteredSearchSuggestionList: {
- template: '<div></div>',
- methods: {
- getValue: () => '=',
- },
- },
- },
+ stubs,
});
+}
describe('LabelToken', () => {
let mock;
@@ -45,7 +61,6 @@ describe('LabelToken', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- wrapper = createComponent();
});
afterEach(() => {
@@ -98,6 +113,10 @@ describe('LabelToken', () => {
});
describe('methods', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
describe('fetchLabelBySearchTerm', () => {
it('calls `config.fetchLabels` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels');
@@ -140,6 +159,8 @@ describe('LabelToken', () => {
});
describe('template', () => {
+ const defaultLabels = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
@@ -166,5 +187,58 @@ describe('LabelToken', () => {
.attributes('style'),
).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);');
});
+
+ it('renders provided defaultLabels as suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockLabelToken, defaultLabels },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultLabels.length);
+ defaultLabels.forEach((label, index) => {
+ expect(suggestions.at(index).text()).toBe(label.text);
+ });
+ });
+
+ it('does not render divider when no defaultLabels', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockLabelToken, defaultLabels: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders `DEFAULT_LABELS` as default suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockLabelToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(DEFAULT_LABELS.length);
+ DEFAULT_LABELS.forEach((label, index) => {
+ expect(suggestions.at(index).text()).toBe(label.text);
+ });
+ });
});
});
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 de893bf44c8..0ec814e3f15 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
@@ -1,10 +1,16 @@
import { mount } from '@vue/test-utils';
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchTokenSegment,
+ GlDropdownDivider,
+} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
+import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import {
@@ -16,12 +22,24 @@ import {
jest.mock('~/flash');
-const createComponent = ({
- config = mockMilestoneToken,
- value = { data: '' },
- active = false,
-} = {}) =>
- mount(MilestoneToken, {
+const defaultStubs = {
+ Portal: true,
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+};
+
+function createComponent(options = {}) {
+ const {
+ config = mockMilestoneToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(MilestoneToken, {
propsData: {
config,
value,
@@ -31,18 +49,9 @@ const createComponent = ({
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
},
- stubs: {
- Portal: {
- template: '<div><slot></slot></div>',
- },
- GlFilteredSearchSuggestionList: {
- template: '<div></div>',
- methods: {
- getValue: () => '=',
- },
- },
- },
+ stubs,
});
+}
describe('MilestoneToken', () => {
let mock;
@@ -128,6 +137,8 @@ describe('MilestoneToken', () => {
});
describe('template', () => {
+ const defaultMilestones = [{ text: 'foo', value: 'foo' }, { text: 'bar', value: 'baz' }];
+
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
@@ -146,7 +157,60 @@ describe('MilestoneToken', () => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"'
- expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1"
+ expect(tokenSegments.at(2).text()).toBe(`%${mockRegularMilestone.title}`); // "4.0 RC1"
+ });
+
+ it('renders provided defaultMilestones as suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockMilestoneToken, defaultMilestones },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(defaultMilestones.length);
+ defaultMilestones.forEach((milestone, index) => {
+ expect(suggestions.at(index).text()).toBe(milestone.text);
+ });
+ });
+
+ it('does not render divider when no defaultMilestones', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockMilestoneToken, defaultMilestones: [] },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.contains(GlFilteredSearchSuggestion)).toBe(false);
+ expect(wrapper.contains(GlDropdownDivider)).toBe(false);
+ });
+
+ it('renders `DEFAULT_MILESTONES` as default suggestions', async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockMilestoneToken },
+ stubs: { Portal: true },
+ });
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions).toHaveLength(DEFAULT_MILESTONES.length);
+ DEFAULT_MILESTONES.forEach((milestone, index) => {
+ expect(suggestions.at(index).text()).toBe(milestone.text);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
deleted file mode 100644
index 87cafa0bb8c..00000000000
--- a/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
+++ /dev/null
@@ -1,190 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/vue_shared/components/filtered_search_dropdown.vue';
-
-describe('Filtered search dropdown', () => {
- const Component = Vue.extend(component);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('with an empty array of items', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [],
- filterKey: '',
- });
- });
-
- it('renders empty list', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
- });
-
- it('renders filter input', () => {
- expect(vm.$el.querySelector('.js-filtered-dropdown-input')).not.toBeNull();
- });
- });
-
- describe('when visible numbers is less than the items length', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
- visibleItems: 2,
- filterKey: 'title',
- });
- });
-
- it('it renders only the maximum number provided', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
- });
- });
-
- describe('when visible number is bigger than the items length', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
- filterKey: 'title',
- });
- });
-
- it('it renders the full list of items the maximum number provided', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(3);
- });
- });
-
- describe('while filtering', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- });
- });
-
- it('updates the results to match the typed value', done => {
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'three';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
- done();
- });
- });
-
- describe('when no value matches the typed one', () => {
- it('does not render any result', done => {
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'six';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
- done();
- });
- });
- });
- });
-
- describe('with create mode enabled', () => {
- describe('when there are no matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- showCreateMode: true,
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('renders a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).not.toBeNull();
- done();
- });
- });
-
- it('renders computed button text', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button').textContent.trim()).toEqual(
- 'Create eleven',
- );
- done();
- });
- });
-
- describe('on click create button', () => {
- it('emits createItem event with the filter', done => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.$nextTick(() => {
- vm.$el.querySelector('.js-dropdown-create-button').click();
-
- expect(vm.$emit).toHaveBeenCalledWith('createItem', 'eleven');
- done();
- });
- });
- });
- });
-
- describe('when there are matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- showCreateMode: true,
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'one';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('does not render a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
- done();
- });
- });
- });
- });
-
- describe('with create mode disabled', () => {
- describe('when there are no matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('does not render a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
- done();
- });
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/icon_spec.js b/spec/frontend/vue_shared/components/icon_spec.js
deleted file mode 100644
index 16728e1705a..00000000000
--- a/spec/frontend/vue_shared/components/icon_spec.js
+++ /dev/null
@@ -1,78 +0,0 @@
-import Vue from 'vue';
-import { mount } from '@vue/test-utils';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import iconsPath from '@gitlab/svgs/dist/icons.svg';
-import Icon from '~/vue_shared/components/icon.vue';
-
-jest.mock('@gitlab/svgs/dist/icons.svg', () => 'testing');
-
-describe('Sprite Icon Component', () => {
- describe('Initialization', () => {
- let icon;
-
- beforeEach(() => {
- const IconComponent = Vue.extend(Icon);
-
- icon = mountComponent(IconComponent, {
- name: 'commit',
- size: 32,
- });
- });
-
- afterEach(() => {
- icon.$destroy();
- });
-
- it('should return a defined Vue component', () => {
- expect(icon).toBeDefined();
- });
-
- it('should have <svg> as a child element', () => {
- expect(icon.$el.tagName).toBe('svg');
- });
-
- it('should have <use> as a child element with the correct href', () => {
- expect(icon.$el.firstChild.tagName).toBe('use');
- expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${iconsPath}#commit`);
- });
-
- it('should properly compute iconSizeClass', () => {
- expect(icon.iconSizeClass).toBe('s32');
- });
-
- it('forbids invalid size prop', () => {
- expect(icon.$options.props.size.validator(NaN)).toBeFalsy();
- expect(icon.$options.props.size.validator(0)).toBeFalsy();
- expect(icon.$options.props.size.validator(9001)).toBeFalsy();
- });
-
- it('should properly render img css', () => {
- const { classList } = icon.$el;
- const containsSizeClass = classList.contains('s32');
-
- expect(containsSizeClass).toBe(true);
- });
-
- it('`name` validator should return false for non existing icons', () => {
- jest.spyOn(console, 'warn').mockImplementation();
-
- expect(Icon.props.name.validator('non_existing_icon_sprite')).toBe(false);
- });
-
- it('`name` validator should return true for existing icons', () => {
- expect(Icon.props.name.validator('commit')).toBe(true);
- });
- });
-
- it('should call registered listeners when they are triggered', () => {
- const clickHandler = jest.fn();
- const wrapper = mount(Icon, {
- propsData: { name: 'commit' },
- listeners: { click: clickHandler },
- });
-
- wrapper.find('svg').trigger('click');
-
- expect(clickHandler).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
index b72f78c4f60..c87d19df1f7 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { mockMilestone } from 'jest/boards/mock_data';
+import { GlIcon } from '@gitlab/ui';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const createComponent = (milestone = mockMilestone) => {
const Component = Vue.extend(IssueMilestone);
@@ -135,7 +135,7 @@ describe('IssueMilestoneComponent', () => {
});
it('renders milestone icon', () => {
- expect(wrapper.find(Icon).props('name')).toBe('clock');
+ expect(wrapper.find(GlIcon).props('name')).toBe('clock');
});
it('renders milestone title', () => {
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index fb9487d0bf8..2319bf61482 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -35,6 +35,9 @@ describe('RelatedIssuableItem', () => {
weight: '<div class="js-weight-slot"></div>',
};
+ const findRemoveButton = () => wrapper.find({ ref: 'removeButton' });
+ const findLockIcon = () => wrapper.find({ ref: 'lockIcon' });
+
beforeEach(() => {
mountComponent({ props, slots });
});
@@ -121,10 +124,10 @@ describe('RelatedIssuableItem', () => {
});
it('renders milestone icon and name', () => {
- const milestoneIcon = tokenMetadata().find('.item-milestone svg use');
+ const milestoneIcon = tokenMetadata().find('.item-milestone svg');
const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title');
- expect(milestoneIcon.attributes('href')).toContain('clock');
+ expect(milestoneIcon.attributes('data-testid')).toBe('clock-icon');
expect(milestoneTitle.text()).toContain('Milestone title');
});
@@ -143,25 +146,27 @@ describe('RelatedIssuableItem', () => {
});
describe('remove button', () => {
- const removeButton = () => wrapper.find({ ref: 'removeButton' });
-
beforeEach(() => {
wrapper.setProps({ canRemove: true });
});
it('renders if canRemove', () => {
- expect(removeButton().exists()).toBe(true);
+ expect(findRemoveButton().exists()).toBe(true);
+ });
+
+ it('does not render the lock icon', () => {
+ expect(findLockIcon().exists()).toBe(false);
});
it('renders disabled button when removeDisabled', async () => {
wrapper.setData({ removeDisabled: true });
await wrapper.vm.$nextTick();
- expect(removeButton().attributes('disabled')).toEqual('disabled');
+ expect(findRemoveButton().attributes('disabled')).toEqual('disabled');
});
it('triggers onRemoveRequest when clicked', async () => {
- removeButton().trigger('click');
+ findRemoveButton().trigger('click');
await wrapper.vm.$nextTick();
const { relatedIssueRemoveRequest } = wrapper.emitted();
@@ -169,4 +174,23 @@ describe('RelatedIssuableItem', () => {
expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
});
});
+
+ describe('when issue is locked', () => {
+ const lockedMessage = 'Issues created from a vulnerability cannot be removed';
+
+ beforeEach(() => {
+ wrapper.setProps({
+ isLocked: true,
+ lockedMessage,
+ });
+ });
+
+ it('does not render the remove button', () => {
+ expect(findRemoveButton().exists()).toBe(false);
+ });
+
+ it('renders the lock icon with the correct title', () => {
+ expect(findLockIcon().attributes('title')).toBe(lockedMessage);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 551d781d296..82bc9b9fe08 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -22,6 +22,12 @@ describe('Markdown field header component', () => {
.at(0);
beforeEach(() => {
+ window.gl = {
+ client: {
+ isMac: true,
+ },
+ };
+
createWrapper();
});
@@ -30,24 +36,40 @@ describe('Markdown field header component', () => {
wrapper = null;
});
- it('renders markdown header buttons', () => {
- const buttons = [
- 'Add bold text',
- 'Add italic text',
- 'Insert a quote',
- 'Insert suggestion',
- 'Insert code',
- 'Add a link',
- 'Add a bullet list',
- 'Add a numbered list',
- 'Add a task list',
- 'Add a table',
- 'Go full screen',
- ];
- const elements = findToolbarButtons();
-
- elements.wrappers.forEach((buttonEl, index) => {
- expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
+ describe('markdown header buttons', () => {
+ it('renders the buttons with the correct title', () => {
+ const buttons = [
+ 'Add bold text (⌘B)',
+ 'Add italic text (⌘I)',
+ '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 table',
+ 'Go full screen',
+ ];
+ const elements = findToolbarButtons();
+
+ elements.wrappers.forEach((buttonEl, index) => {
+ expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
+ });
+ });
+
+ describe('when the user is on a non-Mac', () => {
+ beforeEach(() => {
+ delete window.gl.client.isMac;
+
+ createWrapper();
+ });
+
+ it('renders keyboard shortcuts with Ctrl+ instead of ⌘', () => {
+ const boldButton = findToolbarButtonByProp('icon', 'bold');
+
+ expect(boldButton.props('buttonTitle')).toBe('Add bold text (Ctrl+B)');
+ });
});
});
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 c6e147899e4..a521668b15c 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
@@ -77,10 +77,7 @@ describe('Suggestion Diff component', () => {
});
it('emits apply', () => {
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'apply',
- args: [expect.any(Function)],
- });
+ expect(wrapper.emitted().apply).toEqual([[expect.any(Function)]]);
});
it('does not render apply suggestion and add to batch buttons', () => {
@@ -111,10 +108,7 @@ describe('Suggestion Diff component', () => {
findAddToBatchButton().vm.$emit('click');
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'addToBatch',
- args: [],
- });
+ expect(wrapper.emitted().addToBatch).toEqual([[]]);
});
});
@@ -124,10 +118,7 @@ describe('Suggestion Diff component', () => {
findRemoveFromBatchButton().vm.$emit('click');
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'removeFromBatch',
- args: [],
- });
+ expect(wrapper.emitted().removeFromBatch).toEqual([[]]);
});
});
@@ -137,10 +128,7 @@ describe('Suggestion Diff component', () => {
findApplyBatchButton().vm.$emit('click');
- expect(wrapper.emittedByOrder()).toContainEqual({
- name: 'applyBatch',
- args: [],
- });
+ expect(wrapper.emitted().applyBatch).toEqual([[]]);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
index 6ae405017c9..b67f4cf12bf 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
@@ -34,12 +34,25 @@ describe('SuggestionDiffRow', () => {
const findOldLineWrapper = () => wrapper.find('.old_line');
const findNewLineWrapper = () => wrapper.find('.new_line');
+ const findSuggestionContent = () => wrapper.find('[data-testid="suggestion-diff-content"]');
afterEach(() => {
wrapper.destroy();
});
describe('renders correctly', () => {
+ it('renders the correct base suggestion markup', () => {
+ factory({
+ propsData: {
+ line: oldLine,
+ },
+ });
+
+ expect(findSuggestionContent().html()).toBe(
+ '<td data-testid="suggestion-diff-content" class="line_content old"><span class="line">oldrichtext</span></td>',
+ );
+ });
+
it('has the right classes on the wrapper', () => {
factory({
propsData: {
@@ -47,7 +60,12 @@ describe('SuggestionDiffRow', () => {
},
});
- expect(wrapper.is('.line_holder')).toBe(true);
+ expect(wrapper.classes()).toContain('line_holder');
+ expect(
+ findSuggestionContent()
+ .find('span')
+ .classes(),
+ ).toContain('line');
});
it('renders the rich text when it is available', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
new file mode 100644
index 00000000000..8a7946fd7b1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
+
+describe('toolbar_button', () => {
+ let wrapper;
+
+ const defaultProps = {
+ buttonTitle: 'test button',
+ icon: 'rocket',
+ tag: 'test tag',
+ };
+
+ const createComponent = propUpdates => {
+ wrapper = shallowMount(ToolbarButton, {
+ propsData: {
+ ...defaultProps,
+ ...propUpdates,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const getButtonShortcutsAttr = () => {
+ return wrapper.find('button').attributes('data-md-shortcuts');
+ };
+
+ describe('keyboard shortcuts', () => {
+ it.each`
+ shortcutsProp | mdShortcutsAttr
+ ${undefined} | ${JSON.stringify([])}
+ ${[]} | ${JSON.stringify([])}
+ ${'command+b'} | ${JSON.stringify(['command+b'])}
+ ${['command+b', 'ctrl+b']} | ${JSON.stringify(['command+b', 'ctrl+b'])}
+ `(
+ 'adds the attribute data-md-shortcuts="$mdShortcutsAttr" to the button when the shortcuts prop is $shortcutsProp',
+ ({ shortcutsProp, mdShortcutsAttr }) => {
+ createComponent({ shortcuts: shortcutsProp });
+
+ expect(getButtonShortcutsAttr()).toBe(mdShortcutsAttr);
+ },
+ );
+ });
+});
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 ae8c9a0928e..61660f79b71 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
-import Icon from '~/vue_shared/components/icon.vue';
describe('Issue Warning Component', () => {
let wrapper;
- const findIcon = (w = wrapper) => w.find(Icon);
+ 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' });
@@ -69,7 +69,7 @@ describe('Issue Warning Component', () => {
});
it('renders warning icon', () => {
- expect(wrapper.find(Icon).exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
});
it('does not render information about locked noteable', () => {
@@ -95,7 +95,7 @@ describe('Issue Warning Component', () => {
});
it('does not render warning icon', () => {
- expect(wrapper.find(Icon).exists()).toBe(false);
+ expect(wrapper.find(GlIcon).exists()).toBe(false);
});
it('does not render information about locked noteable', () => {
diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
index f73d3edec5d..bd4b6a463ab 100644
--- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
+++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
@@ -17,9 +17,9 @@ describe(`TimelineEntryItem`, () => {
it('renders correctly', () => {
factory();
- expect(wrapper.is('.timeline-entry')).toBe(true);
+ expect(wrapper.classes()).toContain('timeline-entry');
- expect(wrapper.contains('.timeline-entry-inner')).toBe(true);
+ expect(wrapper.find('.timeline-entry-inner').exists()).toBe(true);
});
it('accepts default slot', () => {
diff --git a/spec/frontend/vue_shared/components/ordered_layout_spec.js b/spec/frontend/vue_shared/components/ordered_layout_spec.js
index e8667d9ee4a..eec153c3792 100644
--- a/spec/frontend/vue_shared/components/ordered_layout_spec.js
+++ b/spec/frontend/vue_shared/components/ordered_layout_spec.js
@@ -27,7 +27,9 @@ describe('Ordered Layout', () => {
let wrapper;
const verifyOrder = () =>
- wrapper.findAll('footer,header').wrappers.map(x => (x.is('footer') ? 'footer' : 'header'));
+ wrapper
+ .findAll('footer,header')
+ .wrappers.map(x => (x.element.tagName === 'FOOTER' ? 'footer' : 'header'));
const createComponent = (props = {}) => {
wrapper = mount(TestComponent, {
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index 46e45296c37..c0ee49f194f 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -48,7 +48,7 @@ describe('Pagination links component', () => {
describe('rendering', () => {
it('it renders the gl-paginated-list', () => {
- expect(wrapper.contains('ul.list-group')).toBe(true);
+ expect(wrapper.find('ul.list-group').exists()).toBe(true);
expect(wrapper.findAll('li.list-group-item').length).toBe(2);
});
});
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 385134c4a3f..649eb2643f1 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
@@ -29,7 +29,7 @@ describe('ProjectListItem component', () => {
it('does not render a check mark icon if selected === false', () => {
wrapper = shallowMount(Component, options);
- expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true);
+ expect(wrapper.find('.js-selected-icon').exists()).toBe(false);
});
it('renders a check mark icon if selected === true', () => {
@@ -37,7 +37,7 @@ describe('ProjectListItem component', () => {
wrapper = shallowMount(Component, options);
- expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true);
+ expect(wrapper.find('.js-selected-icon').exists()).toBe(true);
});
it(`emits a "clicked" event when clicked`, () => {
@@ -53,7 +53,7 @@ describe('ProjectListItem component', () => {
it(`renders the project avatar`, () => {
wrapper = shallowMount(Component, options);
- expect(wrapper.contains('.js-project-avatar')).toBe(true);
+ expect(wrapper.find('.js-project-avatar').exists()).toBe(true);
});
it(`renders a simple namespace name with a trailing slash`, () => {
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
new file mode 100644
index 00000000000..16094a42668
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Package code instruction multiline to match the snapshot 1`] = `
+<div>
+ <pre
+ class="gl-font-monospace"
+ data-testid="multiline-instruction"
+ >
+ this is some
+multiline text
+ </pre>
+</div>
+`;
+
+exports[`Package code instruction single line to match the default snapshot 1`] = `
+<div
+ class="gl-mb-3"
+>
+ <label
+ for="instruction-input_2"
+ >
+ foo_label
+ </label>
+
+ <div
+ class="input-group gl-mb-3"
+ >
+ <input
+ class="form-control gl-font-monospace"
+ data-testid="instruction-input"
+ id="instruction-input_2"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ data-testid="instruction-button"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-md btn-default"
+ data-clipboard-text="npm i @my-package"
+ title="Copy npm install command"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ class="gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
index a1751d69c70..2abae33bc19 100644
--- a/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`History Element renders the correct markup 1`] = `
+exports[`History Item renders the correct markup 1`] = `
<li
class="timeline-entry system-note note-wrapper gl-mb-6!"
>
@@ -31,7 +31,11 @@ exports[`History Element renders the correct markup 1`] = `
<div
class="note-body"
- />
+ >
+ <div
+ data-testid="body-slot"
+ />
+ </div>
</div>
</div>
</li>
diff --git a/spec/frontend/packages/details/components/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 724eddb9070..84c738764a3 100644
--- a/spec/frontend/packages/details/components/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
-import CodeInstruction from '~/packages/details/components/code_instruction.vue';
-import { TrackingLabels } from '~/packages/details/constants';
import Tracking from '~/tracking';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('Package code instruction', () => {
let wrapper;
@@ -20,16 +20,20 @@ describe('Package code instruction', () => {
});
}
- const findInstructionInput = () => wrapper.find('.js-instruction-input');
- const findInstructionPre = () => wrapper.find('.js-instruction-pre');
- const findInstructionButton = () => wrapper.find('.js-instruction-button');
+ const findCopyButton = () => wrapper.find(ClipboardButton);
+ const findInputElement = () => wrapper.find('[data-testid="instruction-input"]');
+ const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]');
afterEach(() => {
wrapper.destroy();
});
describe('single line', () => {
- beforeEach(() => createComponent());
+ beforeEach(() =>
+ createComponent({
+ label: 'foo_label',
+ }),
+ );
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
@@ -41,6 +45,7 @@ describe('Package code instruction', () => {
createComponent({
instruction: 'this is some\nmultiline text',
copyText: 'Copy the command',
+ label: 'foo_label',
multiline: true,
}),
);
@@ -53,7 +58,7 @@ describe('Package code instruction', () => {
describe('tracking', () => {
let eventSpy;
const trackingAction = 'test_action';
- const label = TrackingLabels.CODE_INSTRUCTION;
+ const trackingLabel = 'foo_label';
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
@@ -61,7 +66,7 @@ describe('Package code instruction', () => {
it('should not track when no trackingAction is provided', () => {
createComponent();
- findInstructionButton().trigger('click');
+ findCopyButton().trigger('click');
expect(eventSpy).toHaveBeenCalledTimes(0);
});
@@ -70,22 +75,23 @@ describe('Package code instruction', () => {
beforeEach(() =>
createComponent({
trackingAction,
+ trackingLabel,
}),
);
it('should track when copying from the input', () => {
- findInstructionInput().trigger('copy');
+ findInputElement().trigger('copy');
expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
- label,
+ label: trackingLabel,
});
});
it('should track when the copy button is pressed', () => {
- findInstructionButton().trigger('click');
+ findCopyButton().trigger('click');
expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
- label,
+ label: trackingLabel,
});
});
});
@@ -94,15 +100,16 @@ describe('Package code instruction', () => {
beforeEach(() =>
createComponent({
trackingAction,
+ trackingLabel,
multiline: true,
}),
);
it('should track when copying from the multiline pre element', () => {
- findInstructionPre().trigger('copy');
+ findMultilineInstruction().trigger('copy');
expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
- label,
+ label: trackingLabel,
});
});
});
diff --git a/spec/frontend/registry/shared/components/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js
index 5ae4e0ab37f..16a55b84787 100644
--- a/spec/frontend/registry/shared/components/details_row_spec.js
+++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
-import component from '~/registry/shared/components/details_row.vue';
+import component from '~/vue_shared/components/registry/details_row.vue';
describe('DetailsRow', () => {
let wrapper;
diff --git a/spec/frontend/packages/details/components/history_element_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js
index e8746fc93f5..d51ddda2e3e 100644
--- a/spec/frontend/packages/details/components/history_element_spec.js
+++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
-import component from '~/packages/details/components/history_element.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import component from '~/vue_shared/components/registry/history_item.vue';
-describe('History Element', () => {
+describe('History Item', () => {
let wrapper;
const defaultProps = {
icon: 'pencil',
@@ -17,6 +17,7 @@ describe('History Element', () => {
},
slots: {
default: '<div data-testid="default-slot"></div>',
+ body: '<div data-testid="body-slot"></div>',
},
});
};
@@ -29,6 +30,7 @@ describe('History Element', () => {
const findTimelineEntry = () => wrapper.find(TimelineEntryItem);
const findGlIcon = () => wrapper.find(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+ const findBodySlot = () => wrapper.find('[data-testid="body-slot"]');
it('renders the correct markup', () => {
mountComponent();
@@ -41,11 +43,19 @@ describe('History Element', () => {
expect(findDefaultSlot().exists()).toBe(true);
});
+
+ it('has a body slot', () => {
+ mountComponent();
+
+ expect(findBodySlot().exists()).toBe(true);
+ });
+
it('has a timeline entry', () => {
mountComponent();
expect(findTimelineEntry().exists()).toBe(true);
});
+
it('has an icon', () => {
mountComponent();
diff --git a/spec/frontend/registry/explorer/components/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index f244627a8c3..e2cfdedb4bf 100644
--- a/spec/frontend/registry/explorer/components/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -1,6 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/list_item.vue';
+import component from '~/vue_shared/components/registry/list_item.vue';
describe('list item', () => {
let wrapper;
@@ -34,7 +34,7 @@ describe('list item', () => {
wrapper = null;
});
- it.each`
+ describe.each`
slotName | finderFunction
${'left-primary'} | ${findLeftPrimarySlot}
${'left-secondary'} | ${findLeftSecondarySlot}
@@ -42,10 +42,18 @@ describe('list item', () => {
${'right-secondary'} | ${findRightSecondarySlot}
${'left-action'} | ${findLeftActionSlot}
${'right-action'} | ${findRightActionSlot}
- `('has a $slotName slot', ({ finderFunction }) => {
- mountComponent();
+ `('$slotName slot', ({ finderFunction, slotName }) => {
+ it('exist when the slot is filled', () => {
+ mountComponent();
+
+ expect(finderFunction().exists()).toBe(true);
+ });
- expect(finderFunction().exists()).toBe(true);
+ it('does not exist when the slot is empty', () => {
+ mountComponent({}, { [slotName]: '' });
+
+ expect(finderFunction().exists()).toBe(false);
+ });
});
describe.each`
@@ -106,51 +114,22 @@ describe('list item', () => {
});
});
- describe('first prop', () => {
- it('when is true displays a double top border', () => {
- mountComponent({ first: true });
-
- expect(wrapper.classes('gl-border-t-2')).toBe(true);
- });
-
- it('when is false display a single top border', () => {
- mountComponent({ first: false });
-
- expect(wrapper.classes('gl-border-t-1')).toBe(true);
- });
- });
-
- describe('last prop', () => {
- it('when is true displays a double bottom border', () => {
- mountComponent({ last: true });
-
- expect(wrapper.classes('gl-border-b-2')).toBe(true);
- });
-
- it('when is false display a single bottom border', () => {
- mountComponent({ last: false });
-
- expect(wrapper.classes('gl-border-b-1')).toBe(true);
- });
- });
-
- describe('selected prop', () => {
- it('when true applies the selected border and background', () => {
- mountComponent({ selected: true });
-
- expect(wrapper.classes()).toEqual(
- expect.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
- );
- expect(wrapper.classes()).toEqual(expect.not.arrayContaining(['gl-border-gray-100']));
- });
-
- it('when false applies the default border', () => {
- mountComponent({ selected: false });
-
- expect(wrapper.classes()).toEqual(
- expect.not.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
- );
- expect(wrapper.classes()).toEqual(expect.arrayContaining(['gl-border-gray-100']));
- });
+ describe('borders and selection', () => {
+ it.each`
+ first | selected | shouldHave | shouldNotHave
+ ${true} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']}
+ ${false} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']}
+ ${true} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']}
+ ${false} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']}
+ `(
+ 'when first is $first and selected is $selected',
+ ({ first, selected, shouldHave, shouldNotHave }) => {
+ mountComponent({ first, selected });
+
+ expect(wrapper.classes()).toEqual(expect.arrayContaining(shouldHave));
+
+ expect(wrapper.classes()).toEqual(expect.not.arrayContaining(shouldNotHave));
+ },
+ );
});
});
diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
new file mode 100644
index 00000000000..ff968ff1831
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
@@ -0,0 +1,101 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLink } from '@gitlab/ui';
+import component from '~/vue_shared/components/registry/metadata_item.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+
+describe('Metadata Item', () => {
+ let wrapper;
+ const defaultProps = {
+ text: 'foo',
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findIcon = () => wrapper.find(GlIcon);
+ const findLink = (w = wrapper) => w.find(GlLink);
+ const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
+ const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate);
+
+ describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', size => {
+ const className = `mw-${size}`;
+
+ it(`${size} is assigned correctly to text`, () => {
+ mountComponent({ ...defaultProps, size });
+
+ expect(findText().classes()).toContain(className);
+ });
+
+ it(`${size} is assigned correctly to link`, () => {
+ mountComponent({ ...defaultProps, link: 'foo', size });
+
+ expect(findTooltipOnTruncate().classes()).toContain(className);
+ });
+ });
+
+ describe('text', () => {
+ it('display a proper text', () => {
+ mountComponent();
+
+ expect(findText().text()).toBe(defaultProps.text);
+ });
+
+ it('uses tooltip_on_truncate', () => {
+ mountComponent();
+
+ const tooltip = findTooltipOnTruncate(findText());
+ expect(tooltip.exists()).toBe(true);
+ expect(tooltip.attributes('title')).toBe(defaultProps.text);
+ });
+ });
+
+ describe('link', () => {
+ it('if a link prop is passed shows a link and hides the text', () => {
+ mountComponent({ ...defaultProps, link: 'bar' });
+
+ expect(findLink().exists()).toBe(true);
+ expect(findText().exists()).toBe(false);
+
+ expect(findLink().attributes('href')).toBe('bar');
+ });
+
+ it('uses tooltip_on_truncate', () => {
+ mountComponent({ ...defaultProps, link: 'bar' });
+
+ const tooltip = findTooltipOnTruncate();
+ expect(tooltip.exists()).toBe(true);
+ expect(tooltip.attributes('title')).toBe(defaultProps.text);
+ expect(findLink(tooltip).exists()).toBe(true);
+ });
+
+ it('hides the link and shows the test if a link prop is not passed', () => {
+ mountComponent();
+
+ expect(findText().exists()).toBe(true);
+ expect(findLink().exists()).toBe(false);
+ });
+ });
+
+ describe('icon', () => {
+ it('if a icon prop is passed shows a icon', () => {
+ mountComponent({ ...defaultProps, icon: 'pencil' });
+
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('pencil');
+ });
+
+ it('if a icon prop is not passed hides the icon', () => {
+ mountComponent();
+
+ expect(findIcon().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
new file mode 100644
index 00000000000..6740d6097a4
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -0,0 +1,98 @@
+import { GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/vue_shared/components/registry/title_area.vue';
+
+describe('title area', () => {
+ let wrapper;
+
+ const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]');
+ const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]');
+ const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`);
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findAvatar = () => wrapper.find(GlAvatar);
+
+ const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ slots: {
+ 'sub-header': '<div data-testid="sub-header" />',
+ 'right-actions': '<div data-testid="right-actions" />',
+ ...slots,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('title', () => {
+ it('if slot is not present defaults to prop', () => {
+ mountComponent();
+
+ expect(findTitle().text()).toBe('foo');
+ });
+ it('if slot is present uses slot', () => {
+ mountComponent({
+ slots: {
+ title: 'slot_title',
+ },
+ });
+ expect(findTitle().text()).toBe('slot_title');
+ });
+ });
+
+ describe('avatar', () => {
+ it('is shown if avatar props exist', () => {
+ mountComponent({ propsData: { title: 'foo', avatar: 'baz' } });
+
+ expect(findAvatar().props('src')).toBe('baz');
+ });
+
+ it('is hidden if avatar props does not exist', () => {
+ mountComponent();
+
+ expect(findAvatar().exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ slotName | finderFunction
+ ${'sub-header'} | ${findSubHeaderSlot}
+ ${'right-actions'} | ${findRightActionsSlot}
+ `('$slotName slot', ({ finderFunction, slotName }) => {
+ it('exist when the slot is filled', () => {
+ mountComponent();
+
+ expect(finderFunction().exists()).toBe(true);
+ });
+
+ it('does not exist when the slot is empty', () => {
+ mountComponent({ slots: { [slotName]: '' } });
+
+ expect(finderFunction().exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ slotNames
+ ${['metadata_foo']}
+ ${['metadata_foo', 'metadata_bar']}
+ ${['metadata_foo', 'metadata_bar', 'metadata_baz']}
+ `('$slotNames metadata slots', ({ slotNames }) => {
+ const slotMocks = slotNames.reduce((acc, current) => {
+ acc[current] = `<div data-testid="${current}" />`;
+ return acc;
+ }, {});
+
+ it('exist when the slot is present', async () => {
+ mountComponent({ slots: slotMocks });
+
+ await wrapper.vm.$nextTick();
+ slotNames.forEach(name => {
+ expect(findMetadataSlot(name).exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
index 2d380b25a0a..78fe6d53eee 100644
--- a/spec/frontend/vue_shared/components/remove_member_modal_spec.js
+++ b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
@@ -48,7 +48,7 @@ describe('RemoveMemberModal', () => {
});
it(`${checkboxTestDescription}`, () => {
- expect(wrapper.contains(GlFormCheckbox)).toBe(checkboxExpected);
+ expect(wrapper.find(GlFormCheckbox).exists()).toBe(checkboxExpected);
});
it('submits the form when the modal is submitted', () => {
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
index 103b53cb280..3990248d021 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
@@ -1,324 +1,433 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Resizable Skeleton Loader default setup renders the bars, labels, and grid with correct position, size, and rx percentages 1`] = `
-<gl-skeleton-loader-stub
- baseurl=""
- height="130"
- preserveaspectratio="xMidYMid meet"
- width="400"
+<div
+ class="gl-px-8"
>
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="30%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="60%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="90%"
- />
-
- <rect
- data-testid="skeleton-chart-bar"
- height="5%"
- rx="0.4%"
- width="6%"
- x="5.875%"
- y="85%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="7%"
- rx="0.4%"
- width="6%"
- x="17.625%"
- y="83%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="9%"
- rx="0.4%"
- width="6%"
- x="29.375%"
- y="81%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="14%"
- rx="0.4%"
- width="6%"
- x="41.125%"
- y="76%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="21%"
- rx="0.4%"
- width="6%"
- x="52.875%"
- y="69%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="35%"
- rx="0.4%"
- width="6%"
- x="64.625%"
- y="55%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="50%"
- rx="0.4%"
- width="6%"
- x="76.375%"
- y="40%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="80%"
- rx="0.4%"
- width="6%"
- x="88.125%"
- y="10%"
- />
-
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="6.875%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="18.625%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="30.375%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="42.125%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="53.875%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="65.625%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="77.375%"
- y="95%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="5%"
- rx="0.4%"
- width="4%"
- x="89.125%"
- y="95%"
- />
-</gl-skeleton-loader-stub>
+ <svg
+ class="gl-skeleton-loader"
+ preserveAspectRatio="xMidYMid meet"
+ version="1.1"
+ viewBox="0 0 400 130"
+ >
+ <rect
+ clip-path="url(#null-idClip)"
+ height="130"
+ style="fill: url(#null-idGradient);"
+ width="400"
+ x="0"
+ y="0"
+ />
+ <defs>
+ <clippath
+ id="null-idClip"
+ >
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="30%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="60%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="90%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="5%"
+ rx="0.4%"
+ width="4%"
+ x="6%"
+ y="85%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="7%"
+ rx="0.4%"
+ width="4%"
+ x="18%"
+ y="83%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="9%"
+ rx="0.4%"
+ width="4%"
+ x="30%"
+ y="81%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="14%"
+ rx="0.4%"
+ width="4%"
+ x="42%"
+ y="76%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="21%"
+ rx="0.4%"
+ width="4%"
+ x="54%"
+ y="69%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="35%"
+ rx="0.4%"
+ width="4%"
+ x="66%"
+ y="55%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="50%"
+ rx="0.4%"
+ width="4%"
+ x="78%"
+ y="40%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="80%"
+ rx="0.4%"
+ width="4%"
+ x="90%"
+ y="10%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="6.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="18.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="30.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="42.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="54.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="66.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="78.5%"
+ y="97%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="3%"
+ rx="0.4%"
+ width="3%"
+ x="90.5%"
+ y="97%"
+ />
+ </clippath>
+ <lineargradient
+ id="null-idGradient"
+ >
+ <stop
+ class="primary-stop"
+ offset="0%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-2; 1"
+ />
+ </stop>
+ <stop
+ class="secondary-stop"
+ offset="50%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1.5; 1.5"
+ />
+ </stop>
+ <stop
+ class="primary-stop"
+ offset="100%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1; 2"
+ />
+ </stop>
+ </lineargradient>
+ </defs>
+ </svg>
+</div>
`;
exports[`Resizable Skeleton Loader with custom settings renders the correct position, and size percentages for bars and labels with different settings 1`] = `
-<gl-skeleton-loader-stub
- baseurl=""
- height="130"
- preserveaspectratio="xMidYMid meet"
- uniquekey=""
- width="400"
+<div
+ class="gl-px-8"
>
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="30%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="60%"
- />
- <rect
- data-testid="skeleton-chart-grid"
- height="1px"
- width="100%"
- x="0"
- y="90%"
- />
-
- <rect
- data-testid="skeleton-chart-bar"
- height="5%"
- rx="0.6%"
- width="3%"
- x="6.0625%"
- y="85%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="7%"
- rx="0.6%"
- width="3%"
- x="18.1875%"
- y="83%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="9%"
- rx="0.6%"
- width="3%"
- x="30.3125%"
- y="81%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="14%"
- rx="0.6%"
- width="3%"
- x="42.4375%"
- y="76%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="21%"
- rx="0.6%"
- width="3%"
- x="54.5625%"
- y="69%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="35%"
- rx="0.6%"
- width="3%"
- x="66.6875%"
- y="55%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="50%"
- rx="0.6%"
- width="3%"
- x="78.8125%"
- y="40%"
- />
- <rect
- data-testid="skeleton-chart-bar"
- height="80%"
- rx="0.6%"
- width="3%"
- x="90.9375%"
- y="10%"
- />
-
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="4.0625%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="16.1875%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="28.3125%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="40.4375%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="52.5625%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="64.6875%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="76.8125%"
- y="98%"
- />
- <rect
- data-testid="skeleton-chart-label"
- height="2%"
- rx="0.6%"
- width="7%"
- x="88.9375%"
- y="98%"
- />
-</gl-skeleton-loader-stub>
+ <svg
+ class="gl-skeleton-loader"
+ preserveAspectRatio="xMidYMid meet"
+ version="1.1"
+ viewBox="0 0 400 130"
+ >
+ <rect
+ clip-path="url(#-idClip)"
+ height="130"
+ style="fill: url(#-idGradient);"
+ width="400"
+ x="0"
+ y="0"
+ />
+ <defs>
+ <clippath
+ id="-idClip"
+ >
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="30%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="60%"
+ />
+ <rect
+ data-testid="skeleton-chart-grid"
+ height="1px"
+ width="100%"
+ x="0"
+ y="90%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="5%"
+ rx="0.6%"
+ width="3%"
+ x="6.0625%"
+ y="85%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="7%"
+ rx="0.6%"
+ width="3%"
+ x="18.1875%"
+ y="83%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="9%"
+ rx="0.6%"
+ width="3%"
+ x="30.3125%"
+ y="81%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="14%"
+ rx="0.6%"
+ width="3%"
+ x="42.4375%"
+ y="76%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="21%"
+ rx="0.6%"
+ width="3%"
+ x="54.5625%"
+ y="69%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="35%"
+ rx="0.6%"
+ width="3%"
+ x="66.6875%"
+ y="55%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="50%"
+ rx="0.6%"
+ width="3%"
+ x="78.8125%"
+ y="40%"
+ />
+ <rect
+ data-testid="skeleton-chart-bar"
+ height="80%"
+ rx="0.6%"
+ width="3%"
+ x="90.9375%"
+ y="10%"
+ />
+
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="4.0625%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="16.1875%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="28.3125%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="40.4375%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="52.5625%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="64.6875%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="76.8125%"
+ y="98%"
+ />
+ <rect
+ data-testid="skeleton-chart-label"
+ height="2%"
+ rx="0.6%"
+ width="7%"
+ x="88.9375%"
+ y="98%"
+ />
+ </clippath>
+ <lineargradient
+ id="-idGradient"
+ >
+ <stop
+ class="primary-stop"
+ offset="0%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-2; 1"
+ />
+ </stop>
+ <stop
+ class="secondary-stop"
+ offset="50%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1.5; 1.5"
+ />
+ </stop>
+ <stop
+ class="primary-stop"
+ offset="100%"
+ >
+ <animate
+ attributeName="offset"
+ dur="1s"
+ repeatCount="indefinite"
+ values="-1; 2"
+ />
+ </stop>
+ </lineargradient>
+ </defs>
+ </svg>
+</div>
`;
diff --git a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
index 7facd02e596..bfc3aeb0303 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
describe('Resizable Skeleton Loader', () => {
let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(ChartSkeletonLoader, {
+ wrapper = mount(ChartSkeletonLoader, {
propsData,
});
};
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
index cafe53e6bb2..a823d04024d 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
@@ -5,8 +5,13 @@ describe('Build Custom Renderer Service', () => {
it('should return an object with the default renderer functions when lacking arguments', () => {
expect(buildCustomHTMLRenderer()).toEqual(
expect.objectContaining({
- list: expect.any(Function),
+ htmlBlock: expect.any(Function),
+ htmlInline: expect.any(Function),
+ heading: expect.any(Function),
+ item: expect.any(Function),
+ paragraph: expect.any(Function),
text: expect.any(Function),
+ softbreak: expect.any(Function),
}),
);
});
@@ -20,8 +25,6 @@ describe('Build Custom Renderer Service', () => {
expect(buildCustomHTMLRenderer(customRenderers)).toEqual(
expect.objectContaining({
html: expect.any(Function),
- list: expect.any(Function),
- text: expect.any(Function),
}),
);
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
index a90d3528d60..fd745c21bb6 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
@@ -1,9 +1,10 @@
import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
+import { attributeDefinition } from './renderers/mock_data';
-describe('HTMLToMarkdownRenderer', () => {
+describe('rich_content_editor/services/html_to_markdown_renderer', () => {
let baseRenderer;
let htmlToMarkdownRenderer;
- const NODE = { nodeValue: 'mock_node' };
+ let fakeNode;
beforeEach(() => {
baseRenderer = {
@@ -12,14 +13,20 @@ describe('HTMLToMarkdownRenderer', () => {
getSpaceControlled: jest.fn(input => `space controlled ${input}`),
convert: jest.fn(),
};
+
+ fakeNode = { nodeValue: 'mock_node', dataset: {} };
+ });
+
+ afterEach(() => {
+ htmlToMarkdownRenderer = null;
});
describe('TEXT_NODE visitor', () => {
it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => {
htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
- expect(htmlToMarkdownRenderer.TEXT_NODE(NODE)).toBe(
- `space controlled trimmed space collapsed ${NODE.nodeValue}`,
+ expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe(
+ `space controlled trimmed space collapsed ${fakeNode.nodeValue}`,
);
});
});
@@ -43,8 +50,8 @@ describe('HTMLToMarkdownRenderer', () => {
baseRenderer.convert.mockReturnValueOnce(list);
- expect(htmlToMarkdownRenderer['LI OL, LI UL'](NODE, list)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list);
+ expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list);
});
});
@@ -62,10 +69,21 @@ describe('HTMLToMarkdownRenderer', () => {
});
baseRenderer.convert.mockReturnValueOnce(listItem);
- expect(htmlToMarkdownRenderer['UL LI'](NODE, listItem)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, listItem);
+ expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, listItem);
},
);
+
+ it('detects attribute definitions and attaches them to the list item', () => {
+ const listItem = '- list item';
+ const result = `${listItem}\n${attributeDefinition}\n`;
+
+ fakeNode.dataset.attributeDefinition = attributeDefinition;
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+ baseRenderer.convert.mockReturnValueOnce(`${listItem}\n`);
+
+ expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
+ });
});
describe('OL LI visitor', () => {
@@ -85,8 +103,8 @@ describe('HTMLToMarkdownRenderer', () => {
});
baseRenderer.convert.mockReturnValueOnce(listItem);
- expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent);
+ expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent);
},
);
});
@@ -105,8 +123,8 @@ describe('HTMLToMarkdownRenderer', () => {
baseRenderer.convert.mockReturnValueOnce(input);
- expect(htmlToMarkdownRenderer['STRONG, B'](NODE, input)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input);
+ expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
},
);
});
@@ -125,9 +143,50 @@ describe('HTMLToMarkdownRenderer', () => {
baseRenderer.convert.mockReturnValueOnce(input);
- expect(htmlToMarkdownRenderer['EM, I'](NODE, input)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input);
+ expect(htmlToMarkdownRenderer['EM, I'](fakeNode, input)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
},
);
});
+
+ describe('H1, H2, H3, H4, H5, H6 visitor', () => {
+ it('detects attribute definitions and attaches them to the heading', () => {
+ const heading = 'heading text';
+ const result = `${heading.trimRight()}\n${attributeDefinition}\n\n`;
+
+ fakeNode.dataset.attributeDefinition = attributeDefinition;
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+ baseRenderer.convert.mockReturnValueOnce(`${heading}\n\n`);
+
+ expect(htmlToMarkdownRenderer['H1, H2, H3, H4, H5, H6'](fakeNode, heading)).toBe(result);
+ });
+ });
+
+ describe('PRE CODE', () => {
+ let node;
+ const subContent = 'sub content';
+ const originalConverterResult = 'base result';
+
+ beforeEach(() => {
+ node = document.createElement('PRE');
+
+ node.innerText = 'reference definition content';
+ node.dataset.sseReferenceDefinition = true;
+
+ baseRenderer.convert.mockReturnValueOnce(originalConverterResult);
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
+ });
+
+ it('returns raw text when pre node has sse-reference-definitions class', () => {
+ expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(
+ `\n\n${node.innerText}\n\n`,
+ );
+ });
+
+ it('returns base result when pre node does not have sse-reference-definitions class', () => {
+ delete node.dataset.sseReferenceDefinition;
+
+ expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(originalConverterResult);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
index 660c21281fd..5cf3961819e 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
@@ -1,12 +1,6 @@
// Node spec helpers
-export const buildMockTextNode = literal => {
- return {
- firstChild: null,
- literal,
- type: 'text',
- };
-};
+export const buildMockTextNode = literal => ({ literal, type: 'text' });
export const normalTextNode = buildMockTextNode('This is just normal text.');
@@ -23,17 +17,20 @@ const buildMockUneditableOpenToken = type => {
};
};
-const buildMockUneditableCloseToken = type => {
- return { type: 'closeTag', tagName: type };
+const buildMockTextToken = content => {
+ return {
+ type: 'text',
+ tagName: null,
+ content,
+ };
};
-export const originToken = {
- type: 'text',
- tagName: null,
- content: '{:.no_toc .hidden-md .hidden-lg}',
-};
+const buildMockUneditableCloseToken = type => ({ type: 'closeTag', tagName: type });
+
+export const originToken = buildMockTextToken('{:.no_toc .hidden-md .hidden-lg}');
+const uneditableOpenToken = buildMockUneditableOpenToken('div');
+export const uneditableOpenTokens = [uneditableOpenToken, originToken];
export const uneditableCloseToken = buildMockUneditableCloseToken('div');
-export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
@@ -41,6 +38,7 @@ export const originInlineToken = {
type: 'text',
content: '<i>Inline</i> content',
};
+
export const uneditableInlineTokens = [
buildMockUneditableOpenToken('a'),
originInlineToken,
@@ -48,11 +46,9 @@ export const uneditableInlineTokens = [
];
export const uneditableBlockTokens = [
- buildMockUneditableOpenToken('div'),
- {
- type: 'text',
- tagName: null,
- content: '<div><h1>Some header</h1><p>Some paragraph</p></div>',
- },
- buildMockUneditableCloseToken('div'),
+ uneditableOpenToken,
+ buildMockTextToken('<div><h1>Some header</h1><p>Some paragraph</p></div>'),
+ uneditableCloseToken,
];
+
+export const attributeDefinition = '{:.no_toc .hidden-md .hidden-lg}';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js
new file mode 100644
index 00000000000..69fd9a67a21
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js
@@ -0,0 +1,25 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition';
+import { attributeDefinition } from './mock_data';
+
+describe('rich_content_editor/renderers/render_attribute_definition', () => {
+ describe('canRender', () => {
+ it.each`
+ input | result
+ ${{ literal: attributeDefinition }} | ${true}
+ ${{ literal: `FOO${attributeDefinition}` }} | ${false}
+ ${{ literal: `${attributeDefinition}BAR` }} | ${false}
+ ${{ literal: 'foobar' }} | ${false}
+ `('returns $result when input is $input', ({ input, result }) => {
+ expect(renderer.canRender(input)).toBe(result);
+ });
+ });
+
+ describe('render', () => {
+ it('returns an empty HTML comment', () => {
+ expect(renderer.render()).toEqual({
+ type: 'html',
+ content: '<!-- sse-attribute-definition -->',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js
new file mode 100644
index 00000000000..76abc1ec3d8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js
@@ -0,0 +1,12 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_heading';
+import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+
+describe('rich_content_editor/renderers/render_heading', () => {
+ it('canRender delegates to renderUtils.willAlwaysRender', () => {
+ expect(renderer.canRender).toBe(renderUtils.willAlwaysRender);
+ });
+
+ it('render delegates to renderUtils.renderWithAttributeDefinitions', () => {
+ expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
index f4a06b91a10..b3d9576f38b 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
@@ -1,5 +1,4 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph';
-import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import { buildMockTextNode } from './mock_data';
@@ -17,7 +16,7 @@ const identifierParagraphNode = buildMockParagraphNode(
`[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example2.com`,
);
-describe('Render Identifier Paragraph renderer', () => {
+describe('rich_content_editor/renderers_render_identifier_paragraph', () => {
describe('canRender', () => {
it.each`
node | paragraph | target
@@ -37,8 +36,49 @@ describe('Render Identifier Paragraph renderer', () => {
});
describe('render', () => {
- it('should delegate rendering to the renderUneditableBranch util', () => {
- expect(renderer.render).toBe(renderUneditableBranch);
+ let context;
+ let result;
+
+ beforeEach(() => {
+ const node = {
+ firstChild: {
+ type: 'text',
+ literal: '[Some text]: https://link.com',
+ next: {
+ type: 'linebreak',
+ next: {
+ type: 'text',
+ literal: '[identifier]: http://example1.com "title"',
+ },
+ },
+ },
+ };
+ context = { skipChildren: jest.fn() };
+ result = renderer.render(node, context);
+ });
+
+ it('renders the reference definitions as a code block', () => {
+ expect(result).toEqual([
+ {
+ type: 'openTag',
+ tagName: 'pre',
+ classNames: ['code-block', 'language-markdown'],
+ attributes: {
+ 'data-sse-reference-definition': true,
+ },
+ },
+ { type: 'openTag', tagName: 'code' },
+ {
+ type: 'text',
+ content: '[Some text]: https://link.com\n[identifier]: http://example1.com "title"',
+ },
+ { type: 'closeTag', tagName: 'code' },
+ { type: 'closeTag', tagName: 'pre' },
+ ]);
+ });
+
+ it('skips the reference definition node children from rendering', () => {
+ expect(context.skipChildren).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
deleted file mode 100644
index 7d427108ba6..00000000000
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list';
-import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
-
-import { buildMockTextNode } from './mock_data';
-
-const buildMockListNode = literal => {
- return {
- firstChild: {
- firstChild: {
- firstChild: buildMockTextNode(literal),
- type: 'paragraph',
- },
- type: 'item',
- },
- type: 'list',
- };
-};
-
-const normalListNode = buildMockListNode('Just another bullet point');
-const kramdownListNode = buildMockListNode('TOC');
-
-describe('Render Kramdown List renderer', () => {
- describe('canRender', () => {
- it('should return true when the argument is a special kramdown TOC ordered/unordered list', () => {
- expect(renderer.canRender(kramdownListNode)).toBe(true);
- });
-
- it('should return false when the argument is a normal ordered/unordered list', () => {
- expect(renderer.canRender(normalListNode)).toBe(false);
- });
- });
-
- describe('render', () => {
- it('should delegate rendering to the renderUneditableBranch util', () => {
- expect(renderer.render).toBe(renderUneditableBranch);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
deleted file mode 100644
index 1d2d152ffc3..00000000000
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text';
-import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
-
-import { buildMockTextNode, normalTextNode } from './mock_data';
-
-const kramdownTextNode = buildMockTextNode('{:toc}');
-
-describe('Render Kramdown Text renderer', () => {
- describe('canRender', () => {
- it('should return true when the argument `literal` has kramdown syntax', () => {
- expect(renderer.canRender(kramdownTextNode)).toBe(true);
- });
-
- it('should return false when the argument `literal` lacks kramdown syntax', () => {
- expect(renderer.canRender(normalTextNode)).toBe(false);
- });
- });
-
- describe('render', () => {
- it('should delegate rendering to the renderUneditableLeaf util', () => {
- expect(renderer.render).toBe(renderUneditableLeaf);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js
new file mode 100644
index 00000000000..c1ab700535b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js
@@ -0,0 +1,12 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_list_item';
+import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+
+describe('rich_content_editor/renderers/render_list_item', () => {
+ it('canRender delegates to renderUtils.willAlwaysRender', () => {
+ expect(renderer.canRender).toBe(renderUtils.willAlwaysRender);
+ });
+
+ it('render delegates to renderUtils.renderWithAttributeDefinitions', () => {
+ expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
index 92435b3e4e3..774f830f421 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
@@ -1,6 +1,8 @@
import {
renderUneditableLeaf,
renderUneditableBranch,
+ renderWithAttributeDefinitions,
+ willAlwaysRender,
} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import {
@@ -8,9 +10,9 @@ import {
buildUneditableOpenTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
-import { originToken, uneditableCloseToken } from './mock_data';
+import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data';
-describe('Render utils', () => {
+describe('rich_content_editor/renderers/render_utils', () => {
describe('renderUneditableLeaf', () => {
it('should return uneditable block tokens around an origin token', () => {
const context = { origin: jest.fn().mockReturnValueOnce(originToken) };
@@ -41,4 +43,68 @@ describe('Render utils', () => {
expect(result).toStrictEqual(uneditableCloseToken);
});
});
+
+ describe('willAlwaysRender', () => {
+ it('always returns true', () => {
+ expect(willAlwaysRender()).toBe(true);
+ });
+ });
+
+ describe('renderWithAttributeDefinitions', () => {
+ let openTagToken;
+ let closeTagToken;
+ let node;
+ const attributes = {
+ 'data-attribute-definition': attributeDefinition,
+ };
+
+ beforeEach(() => {
+ openTagToken = { type: 'openTag' };
+ closeTagToken = { type: 'closeTag' };
+ node = {
+ next: {
+ firstChild: {
+ literal: attributeDefinition,
+ },
+ },
+ };
+ });
+
+ describe('when token type is openTag', () => {
+ it('attaches attributes when attributes exist in the node’s next sibling', () => {
+ const context = { origin: () => openTagToken };
+
+ expect(renderWithAttributeDefinitions(node, context)).toEqual({
+ ...openTagToken,
+ attributes,
+ });
+ });
+
+ it('attaches attributes when attributes exist in the node’s children', () => {
+ const context = { origin: () => openTagToken };
+ node = {
+ firstChild: {
+ firstChild: {
+ next: {
+ next: {
+ literal: attributeDefinition,
+ },
+ },
+ },
+ },
+ };
+
+ expect(renderWithAttributeDefinitions(node, context)).toEqual({
+ ...openTagToken,
+ attributes,
+ });
+ });
+ });
+
+ it('does not attach attributes when token type is "closeTag"', () => {
+ const context = { origin: () => closeTagToken };
+
+ expect(renderWithAttributeDefinitions({}, context)).toBe(closeTagToken);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index edec3b138b3..c2091a681f2 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -14,6 +14,7 @@ const createComponent = headerTitle => {
};
describe('DropdownCreateLabelComponent', () => {
+ const colorsCount = Object.keys(mockSuggestedColors).length;
let vm;
beforeEach(() => {
@@ -27,7 +28,7 @@ describe('DropdownCreateLabelComponent', () => {
describe('created', () => {
it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
- expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length);
+ expect(vm.suggestedColors.length).toBe(colorsCount);
});
});
@@ -37,12 +38,10 @@ describe('DropdownCreateLabelComponent', () => {
});
it('renders `Go back` button on component header', () => {
- const backButtonEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-back',
- );
+ const backButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-back');
expect(backButtonEl).not.toBe(null);
- expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null);
+ expect(backButtonEl.querySelector('[data-testid="arrow-left-icon"]')).not.toBe(null);
});
it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => {
@@ -61,12 +60,9 @@ describe('DropdownCreateLabelComponent', () => {
});
it('renders `Close` button on component header', () => {
- const closeButtonEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-close',
- );
+ const closeButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-close');
expect(closeButtonEl).not.toBe(null);
- expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null);
});
it('renders `Name new label` input element', () => {
@@ -78,11 +74,11 @@ describe('DropdownCreateLabelComponent', () => {
const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
expect(colorsListContainerEl).not.toBe(null);
- expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length);
+ expect(colorsListContainerEl.querySelectorAll('a').length).toBe(colorsCount);
const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
- expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]);
+ expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
index 2e721d75b40..0b9a7262e41 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -33,7 +33,7 @@ describe('DropdownHeaderComponent', () => {
);
expect(closeBtnEl).not.toBeNull();
- expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull();
+ expect(closeBtnEl.querySelector('.dropdown-menu-close-icon')).not.toBeNull();
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index e09f0006359..7847e0ee71d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -87,7 +87,7 @@ describe('DropdownValueCollapsedComponent', () => {
});
it('renders tags icon element', () => {
- expect(vm.$el.querySelector('.fa-tags')).not.toBeNull();
+ expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull();
});
it('renders labels count', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
index 6564c012e67..648ba84fe8f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -15,29 +15,29 @@ export const mockLabels = [
},
];
-export const mockSuggestedColors = [
- '#0033CC',
- '#428BCA',
- '#44AD8E',
- '#A8D695',
- '#5CB85C',
- '#69D100',
- '#004E00',
- '#34495E',
- '#7F8C8D',
- '#A295D6',
- '#5843AD',
- '#8E44AD',
- '#FFECDB',
- '#AD4363',
- '#D10069',
- '#CC0033',
- '#FF0000',
- '#D9534F',
- '#D1D100',
- '#F0AD4E',
- '#AD8D43',
-];
+export const mockSuggestedColors = {
+ '#0033CC': 'UA blue',
+ '#428BCA': 'Moderate blue',
+ '#44AD8E': 'Lime green',
+ '#A8D695': 'Feijoa',
+ '#5CB85C': 'Slightly desaturated green',
+ '#69D100': 'Bright green',
+ '#004E00': 'Very dark lime green',
+ '#34495E': 'Very dark desaturated blue',
+ '#7F8C8D': 'Dark grayish cyan',
+ '#A295D6': 'Slightly desaturated blue',
+ '#5843AD': 'Dark moderate blue',
+ '#8E44AD': 'Dark moderate violet',
+ '#FFECDB': 'Very pale orange',
+ '#AD4363': 'Dark moderate pink',
+ '#D10069': 'Strong pink',
+ '#CC0033': 'Strong red',
+ '#FF0000': 'Pure red',
+ '#D9534F': 'Soft red',
+ '#D1D100': 'Strong yellow',
+ '#F0AD4E': 'Soft orange',
+ '#AD8D43': 'Dark moderate orange',
+};
export const mockConfig = {
showCreate: true,
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 cb758797c63..951f706421f 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
@@ -62,7 +62,7 @@ describe('DropdownButton', () => {
describe('template', () => {
it('renders component container element', () => {
- expect(wrapper.is('gl-button-stub')).toBe(true);
+ expect(wrapper.find(GlButton).element).toBe(wrapper.element);
});
it('renders default button text element', () => {
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 a1e0db4d29e..8c17a974b39 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
@@ -65,6 +65,33 @@ describe('LabelsSelectRoot', () => {
]),
);
});
+
+ it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
+ wrapper = createComponent({
+ ...mockConfig,
+ variant: 'embedded',
+ });
+
+ jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
+
+ wrapper.vm.handleVuexActionDispatch(
+ { type: 'toggleDropdownContents' },
+ {
+ showDropdownButton: false,
+ showDropdownContents: false,
+ labels: [{ id: 1 }, { id: 2, set: true }],
+ },
+ );
+
+ expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
+ expect.arrayContaining([
+ {
+ id: 2,
+ set: true,
+ },
+ ]),
+ );
+ });
});
describe('handleDropdownClose', () => {
@@ -123,11 +150,10 @@ describe('LabelsSelectRoot', () => {
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
});
- it('renders `dropdown-value` component with slot when `showDropdownButton` prop is `false`', () => {
+ it('renders `dropdown-value` component', () => {
const wrapperDropdownValue = createComponent(mockConfig, {
default: 'None',
});
- wrapperDropdownValue.vm.$store.state.showDropdownButton = false;
return wrapperDropdownValue.vm.$nextTick(() => {
const valueComp = wrapperDropdownValue.find(DropdownValue);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index c742220ba8a..bfb8e263d81 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -259,6 +259,21 @@ describe('LabelsSelect Actions', () => {
});
});
+ describe('replaceSelectedLabels', () => {
+ it('replaces `state.selectedLabels`', done => {
+ const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
+
+ testAction(
+ actions.replaceSelectedLabels,
+ selectedLabels,
+ state,
+ [{ type: types.REPLACE_SELECTED_LABELS, payload: selectedLabels }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('updateSelectedLabels', () => {
it('updates `state.labels` based on provided `labels` param', done => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index 8081806e314..3414eec8a63 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -152,6 +152,19 @@ describe('LabelsSelect Mutations', () => {
});
});
+ describe(`${types.REPLACE_SELECTED_LABELS}`, () => {
+ it('replaces `state.selectedLabels`', () => {
+ const state = {
+ selectedLabels: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }],
+ };
+ const newSelectedLabels = [{ id: 2 }, { id: 5 }];
+
+ mutations[types.REPLACE_SELECTED_LABELS](state, newSelectedLabels);
+
+ expect(state.selectedLabels).toEqual(newSelectedLabels);
+ });
+ });
+
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index ef3ae088eec..058dfcdbde2 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -34,7 +34,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.isEmpty()).toBe(true);
+ expect(wrapper.html()).toBe('');
});
it('renders if there is a next page', () => {
@@ -50,7 +50,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.isEmpty()).toBe(false);
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
});
it('renders if there is a prev page', () => {
@@ -66,7 +66,7 @@ describe('Pagination component', () => {
change: spy,
});
- expect(wrapper.isEmpty()).toBe(false);
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/todo_button_spec.js b/spec/frontend/vue_shared/components/todo_button_spec.js
new file mode 100644
index 00000000000..482b5de11f6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/todo_button_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
+
+describe('Todo Button', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(TodoButton, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders GlButton', () => {
+ createComponent();
+
+ expect(wrapper.find(GlButton).exists()).toBe(true);
+ });
+
+ it('emits click event when clicked', () => {
+ createComponent({}, mount);
+ wrapper.find(GlButton).trigger('click');
+
+ expect(wrapper.emitted().click).toBeTruthy();
+ });
+
+ it.each`
+ label | isTodo
+ ${'Mark as done'} | ${true}
+ ${'Add a To-Do'} | ${false}
+ `('sets correct label when isTodo is $isTodo', ({ label, isTodo }) => {
+ createComponent({ isTodo });
+
+ expect(wrapper.find(GlButton).text()).toBe(label);
+ });
+
+ it('binds additional props to GlButton', () => {
+ createComponent({ loading: true });
+
+ expect(wrapper.find(GlButton).props('loading')).toBe(true);
+ });
+});
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 902e83da7be..84e7a6a162e 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
@@ -38,10 +38,6 @@ describe('User Avatar Link Component', () => {
wrapper = null;
});
- it('should return a defined Vue component', () => {
- expect(wrapper.isVueInstance()).toBe(true);
- });
-
it('should have user-avatar-image registered as child component', () => {
expect(wrapper.vm.$options.components.userAvatarImage).toBeDefined();
});
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 a4ff6ac0c16..b43bb6b10e0 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,7 +1,6 @@
-import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
-import Icon from '~/vue_shared/components/icon.vue';
const DEFAULT_PROPS = {
user: {
@@ -74,7 +73,7 @@ describe('User Popover Component', () => {
});
it('shows icon for location', () => {
- const iconEl = wrapper.find(Icon);
+ const iconEl = wrapper.find(GlIcon);
expect(iconEl.props('name')).toEqual('location');
});
@@ -139,9 +138,9 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual(
- 1,
- );
+ expect(
+ wrapper.findAll(GlIcon).filter(icon => icon.props('name') === 'profile').length,
+ ).toEqual(1);
});
it('shows icon for work information', () => {
@@ -152,7 +151,9 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1);
+ expect(wrapper.findAll(GlIcon).filter(icon => icon.props('name') === 'work').length).toEqual(
+ 1,
+ );
});
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
new file mode 100644
index 00000000000..57f511903d9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
+
+const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/';
+const TEST_GITPOD_URL = 'https://gitpod.test/';
+
+const ACTION_WEB_IDE = {
+ href: TEST_WEB_IDE_URL,
+ key: 'webide',
+ secondaryText: 'Quickly and easily edit multiple files in your project.',
+ tooltip: '',
+ text: 'Web IDE',
+ attrs: {
+ 'data-qa-selector': 'web_ide_button',
+ },
+};
+const ACTION_WEB_IDE_FORK = {
+ ...ACTION_WEB_IDE,
+ href: '#modal-confirm-fork',
+ handle: expect.any(Function),
+};
+const ACTION_GITPOD = {
+ href: TEST_GITPOD_URL,
+ key: 'gitpod',
+ secondaryText: 'Launch a ready-to-code development environment for your project.',
+ tooltip: 'Launch a ready-to-code development environment for your project.',
+ text: 'Gitpod',
+ attrs: {
+ 'data-qa-selector': 'gitpod_button',
+ },
+};
+const ACTION_GITPOD_ENABLE = {
+ ...ACTION_GITPOD,
+ href: '#modal-enable-gitpod',
+ handle: expect.any(Function),
+};
+
+describe('Web IDE link component', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = shallowMount(WebIdeLink, {
+ propsData: {
+ webIdeUrl: TEST_WEB_IDE_URL,
+ gitpodUrl: TEST_GITPOD_URL,
+ ...props,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findActionsButton = () => wrapper.find(ActionsButton);
+ const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
+
+ it.each`
+ props | expectedActions
+ ${{}} | ${[ACTION_WEB_IDE]}
+ ${{ needsToFork: true }} | ${[ACTION_WEB_IDE_FORK]}
+ ${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: true }} | ${[ACTION_GITPOD]}
+ ${{ showWebIdeButton: false, showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_GITPOD_ENABLE]}
+ ${{ showGitpodButton: true, gitpodEnabled: false }} | ${[ACTION_WEB_IDE, ACTION_GITPOD_ENABLE]}
+ `('renders actions with props=$props', ({ props, expectedActions }) => {
+ createComponent(props);
+
+ expect(findActionsButton().props('actions')).toEqual(expectedActions);
+ });
+
+ describe('with multiple actions', () => {
+ beforeEach(() => {
+ createComponent({ showWebIdeButton: true, showGitpodButton: true, gitpodEnabled: true });
+ });
+
+ it('selected Web IDE by default', () => {
+ expect(findActionsButton().props()).toMatchObject({
+ actions: [ACTION_WEB_IDE, ACTION_GITPOD],
+ selectedKey: ACTION_WEB_IDE.key,
+ });
+ });
+
+ it('should set selection with local storage value', async () => {
+ expect(findActionsButton().props('selectedKey')).toBe(ACTION_WEB_IDE.key);
+
+ findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
+ });
+
+ it('should update local storage when selection changes', async () => {
+ expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key);
+
+ findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findActionsButton().props('selectedKey')).toBe(ACTION_GITPOD.key);
+ expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
+ });
+ });
+});
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index a349aad9f1c..59d05f68fdd 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -11,9 +11,11 @@ describe('App', () => {
let store;
let actions;
let state;
+ let propsData = { features: '[ {"title":"Whats New Drawer"} ]' };
- beforeEach(() => {
+ const buildWrapper = () => {
actions = {
+ openDrawer: jest.fn(),
closeDrawer: jest.fn(),
};
@@ -29,7 +31,12 @@ describe('App', () => {
wrapper = mount(App, {
localVue,
store,
+ propsData,
});
+ };
+
+ beforeEach(() => {
+ buildWrapper();
});
afterEach(() => {
@@ -42,6 +49,10 @@ describe('App', () => {
expect(getDrawer().exists()).toBe(true);
});
+ it('dispatches openDrawer when mounted', () => {
+ expect(actions.openDrawer).toHaveBeenCalled();
+ });
+
it('dispatches closeDrawer when clicking close', () => {
getDrawer().vm.$emit('close');
expect(actions.closeDrawer).toHaveBeenCalled();
@@ -54,4 +65,15 @@ describe('App', () => {
expect(getDrawer().props('open')).toBe(openState);
});
+
+ it('renders features when provided as props', () => {
+ expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
+ });
+
+ it('handles bad json argument gracefully', () => {
+ propsData = { features: 'this is not json' };
+ buildWrapper();
+
+ expect(getDrawer().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/whats_new/components/trigger_spec.js b/spec/frontend/whats_new/components/trigger_spec.js
deleted file mode 100644
index 7961957e077..00000000000
--- a/spec/frontend/whats_new/components/trigger_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createLocalVue, mount } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { GlButton } from '@gitlab/ui';
-import Trigger from '~/whats_new/components/trigger.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('Trigger', () => {
- let wrapper;
- let store;
- let actions;
- let state;
-
- beforeEach(() => {
- actions = {
- openDrawer: jest.fn(),
- };
-
- state = {
- open: true,
- };
-
- store = new Vuex.Store({
- actions,
- state,
- });
-
- wrapper = mount(Trigger, {
- localVue,
- store,
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches openDrawer when clicking close', () => {
- wrapper.find(GlButton).vm.$emit('click');
- expect(actions.openDrawer).toHaveBeenCalled();
- });
-});