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>2021-11-18 16:16:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 16:16:36 +0300
commit311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch)
tree07e7870bca8aed6d61fdcc810731c50d2c40af47 /spec/frontend
parent27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff)
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/experimentation_helper.js29
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js4
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js4
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js4
-rw-r--r--spec/frontend/admin/deploy_keys/components/table_spec.js47
-rw-r--r--spec/frontend/alert_handler_spec.js12
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js18
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js19
-rw-r--r--spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js (renamed from spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js)4
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js150
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js6
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js6
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap14
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js2
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js22
-rw-r--r--spec/frontend/boards/components/board_card_spec.js4
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js33
-rw-r--r--spec/frontend/boards/components/board_form_spec.js18
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js251
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js57
-rw-r--r--spec/frontend/boards/components/new_board_button_spec.js75
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js1
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js11
-rw-r--r--spec/frontend/boards/mock_data.js87
-rw-r--r--spec/frontend/boards/stores/actions_spec.js72
-rw-r--r--spec/frontend/chronic_duration_spec.js354
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js63
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js45
-rw-r--r--spec/frontend/clusters_list/components/agent_empty_state_spec.js16
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js9
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js40
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js55
-rw-r--r--spec/frontend/clusters_list/components/clusters_empty_state_spec.js104
-rw-r--r--spec/frontend/clusters_list/components/clusters_main_view_spec.js82
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js60
-rw-r--r--spec/frontend/clusters_list/components/clusters_view_all_spec.js243
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js38
-rw-r--r--spec/frontend/clusters_list/mocks/apollo.js47
-rw-r--r--spec/frontend/clusters_list/store/mutations_spec.js10
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js18
-rw-r--r--spec/frontend/confirm_modal_spec.js4
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js (renamed from spec/frontend/content_editor/components/content_editor_error_spec.js)30
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js6
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js8
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js8
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js8
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js20
-rw-r--r--spec/frontend/content_editor/extensions/blockquote_spec.js46
-rw-r--r--spec/frontend/content_editor/extensions/emoji_spec.js10
-rw-r--r--spec/frontend/content_editor/extensions/frontmatter_spec.js30
-rw-r--r--spec/frontend/content_editor/extensions/horizontal_rule_spec.js49
-rw-r--r--spec/frontend/content_editor/extensions/inline_diff_spec.js60
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js91
-rw-r--r--spec/frontend/content_editor/extensions/math_inline_spec.js11
-rw-r--r--spec/frontend/content_editor/extensions/table_of_contents_spec.js32
-rw-r--r--spec/frontend/content_editor/extensions/table_spec.js102
-rw-r--r--spec/frontend/content_editor/extensions/word_break_spec.js35
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js4
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js11
-rw-r--r--spec/frontend/content_editor/test_utils.js23
-rw-r--r--spec/frontend/create_merge_request_dropdown_spec.js5
-rw-r--r--spec/frontend/crm/contacts_root_spec.js60
-rw-r--r--spec/frontend/crm/mock_data.js81
-rw-r--r--spec/frontend/crm/organizations_root_spec.js60
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js10
-rw-r--r--spec/frontend/cycle_analytics/metric_popover_spec.js102
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js41
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js144
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js1
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js96
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js58
-rw-r--r--spec/frontend/delete_label_modal_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js10
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js2
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js6
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js2
-rw-r--r--spec/frontend/design_management/pages/index_spec.js14
-rw-r--r--spec/frontend/diffs/components/app_spec.js34
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js23
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js89
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js4
-rw-r--r--spec/frontend/diffs/store/actions_spec.js51
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js24
-rw-r--r--spec/frontend/diffs/utils/diff_line_spec.js30
-rw-r--r--spec/frontend/diffs/utils/discussions_spec.js133
-rw-r--r--spec/frontend/diffs/utils/file_reviews_spec.js24
-rw-r--r--spec/frontend/dropzone_input_spec.js19
-rw-r--r--spec/frontend/editor/helpers.js53
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js68
-rw-r--r--spec/frontend/editor/source_editor_extension_spec.js65
-rw-r--r--spec/frontend/editor/source_editor_instance_spec.js387
-rw-r--r--spec/frontend/editor/source_editor_yaml_ext_spec.js449
-rw-r--r--spec/frontend/environments/graphql/mock_data.js530
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js91
-rw-r--r--spec/frontend/environments/new_environment_folder_spec.js74
-rw-r--r--spec/frontend/environments/new_environments_app_spec.js50
-rw-r--r--spec/frontend/experimentation/utils_spec.js198
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js13
-rw-r--r--spec/frontend/filterable_list_spec.js5
-rw-r--r--spec/frontend/fixtures/api_markdown.yml332
-rw-r--r--spec/frontend/fixtures/projects.rb26
-rw-r--r--spec/frontend/flash_spec.js11
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js12
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js66
-rw-r--r--spec/frontend/google_cloud/components/incubation_banner_spec.js60
-rw-r--r--spec/frontend/google_cloud/components/service_accounts_spec.js79
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js4
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js47
-rw-r--r--spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap2
-rw-r--r--spec/frontend/ide/components/shared/commit_message_field_spec.js149
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js4
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js18
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js33
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js27
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js235
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js96
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js442
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js42
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js61
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js64
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js (renamed from spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js)9
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js26
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js229
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js87
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js303
-rw-r--r--spec/frontend/invite_members/components/confetti_spec.js28
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js210
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js35
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js8
-rw-r--r--spec/frontend/issue_show/components/app_spec.js38
-rw-r--r--spec/frontend/issue_show/components/description_spec.js22
-rw-r--r--spec/frontend/issue_show/components/fields/type_spec.js26
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js2
-rw-r--r--spec/frontend/issues_list/components/new_issue_dropdown_spec.js6
-rw-r--r--spec/frontend/issues_list/mock_data.js86
-rw-r--r--spec/frontend/issues_list/utils_spec.js8
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js44
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js36
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js (renamed from spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js)4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js (renamed from spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js)6
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js176
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js48
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js50
-rw-r--r--spec/frontend/jira_connect/subscriptions/index_spec.js36
-rw-r--r--spec/frontend/jira_connect/subscriptions/utils_spec.js22
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js152
-rw-r--r--spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js150
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js23
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js59
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js10
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js28
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js4
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js8
-rw-r--r--spec/frontend/members/mock_data.js2
-rw-r--r--spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap43
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js423
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/components/alert_widget_form_spec.js242
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js1
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js1
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js106
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js33
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js1
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js10
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js4
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js4
-rw-r--r--spec/frontend/monitoring/router_spec.js3
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js6
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js4
-rw-r--r--spec/frontend/notes/components/multiline_comment_form_spec.js12
-rw-r--r--spec/frontend/notes/components/note_body_spec.js1
-rw-r--r--spec/frontend/notes/components/note_form_spec.js2
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js10
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js59
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js61
-rw-r--r--spec/frontend/notes/stores/actions_spec.js91
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js10
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap6
-rw-r--r--spec/frontend/packages/list/components/packages_list_app_spec.js45
-rw-r--r--spec/frontend/packages/list/components/packages_search_spec.js128
-rw-r--r--spec/frontend/packages/list/components/packages_title_spec.js71
-rw-r--r--spec/frontend/packages/list/components/tokens/package_type_token_spec.js48
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap (renamed from spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap)62
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js (renamed from spec/frontend/registry/explorer/components/delete_button_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js (renamed from spec/frontend/registry/explorer/components/delete_image_spec.js)8
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap (renamed from spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/details_header_spec.js)8
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/empty_state_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js)7
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/status_alert_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/tags_list_spec.js)15
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap (renamed from spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap (renamed from spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js)8
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/image_list_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/registry_header_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js (renamed from spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js (renamed from spec/frontend/registry/explorer/mock_data.js)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js (renamed from spec/frontend/registry/explorer/pages/details_spec.js)24
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js (renamed from spec/frontend/registry/explorer/pages/index_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js (renamed from spec/frontend/registry/explorer/pages/list_spec.js)22
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/stubs.js (renamed from spec/frontend/registry/explorer/stubs.js)2
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js99
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js59
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/mock_data.js21
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap1
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js53
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js160
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap57
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js168
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js244
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js17
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/shared/mocks.js (renamed from spec/frontend/registry/shared/mocks.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/stubs.js (renamed from spec/frontend/registry/shared/stubs.js)0
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js8
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js4
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap7
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js33
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js26
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js19
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js16
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js37
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js38
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js39
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js72
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js132
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js29
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js82
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js117
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js106
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js83
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js6
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js6
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js15
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js2
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap41
-rw-r--r--spec/frontend/projects/components/project_delete_button_spec.js12
-rw-r--r--spec/frontend/projects/details/upload_button_spec.js7
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js39
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js24
-rw-r--r--spec/frontend/projects/projects_filterable_list_spec.js5
-rw-r--r--spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js98
-rw-r--r--spec/frontend/projects/settings_service_desk/components/mock_data.js8
-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.js80
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js80
-rw-r--r--spec/frontend/projects/storage_counter/components/storage_table_spec.js5
-rw-r--r--spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js41
-rw-r--r--spec/frontend/projects/storage_counter/mock_data.js33
-rw-r--r--spec/frontend/projects/storage_counter/utils_spec.js17
-rw-r--r--spec/frontend/projects/upload_file_experiment_tracking_spec.js43
-rw-r--r--spec/frontend/related_merge_requests/components/related_merge_requests_spec.js4
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js4
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js399
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js10
-rw-r--r--spec/frontend/repository/mock_data.js57
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js39
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js27
-rw-r--r--spec/frontend/runner/components/cells/runner_status_cell_spec.js69
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js39
-rw-r--r--spec/frontend/runner/components/cells/runner_type_cell_spec.js48
-rw-r--r--spec/frontend/runner/components/helpers/masked_value_spec.js51
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js169
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js (renamed from spec/frontend/runner/components/runner_registration_token_reset_spec.js)45
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js109
-rw-r--r--spec/frontend/runner/components/runner_contacted_state_badge_spec.js86
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js60
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js59
-rw-r--r--spec/frontend/runner/components/runner_manual_setup_help_spec.js122
-rw-r--r--spec/frontend/runner/components/runner_paused_badge_spec.js (renamed from spec/frontend/runner/components/runner_state_paused_badge_spec.js)2
-rw-r--r--spec/frontend/runner/components/runner_state_locked_badge_spec.js45
-rw-r--r--spec/frontend/runner/components/runner_tag_spec.js46
-rw-r--r--spec/frontend/runner/components/runner_tags_spec.js10
-rw-r--r--spec/frontend/runner/components/runner_type_alert_spec.js14
-rw-r--r--spec/frontend/runner/components/runner_type_badge_spec.js14
-rw-r--r--spec/frontend/runner/components/runner_type_tabs_spec.js109
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js37
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js36
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js56
-rw-r--r--spec/frontend/search/store/actions_spec.js31
-rw-r--r--spec/frontend/search/store/mutations_spec.js10
-rw-r--r--spec/frontend/search/store/utils_spec.js29
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js43
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js11
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js21
-rw-r--r--spec/frontend/sidebar/components/attention_required_toggle_spec.js84
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js20
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js2
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js55
-rw-r--r--spec/frontend/task_list_spec.js17
-rw-r--r--spec/frontend/terms/components/app_spec.js171
-rw-r--r--spec/frontend/test_setup.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js53
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/actions_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js25
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js11
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js13
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js2
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js28
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js9
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/test_extension.js2
-rw-r--r--spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js99
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js26
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js67
-rw-r--r--spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js121
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js57
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js2
-rw-r--r--spec/frontend/whats_new/utils/notification_spec.js6
-rw-r--r--spec/frontend/work_items/components/app_spec.js24
-rw-r--r--spec/frontend/work_items/mock_data.js17
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js70
-rw-r--r--spec/frontend/work_items/router_spec.js30
374 files changed, 12347 insertions, 5440 deletions
diff --git a/spec/frontend/__helpers__/experimentation_helper.js b/spec/frontend/__helpers__/experimentation_helper.js
index 7a2ef61216a..e0156226acc 100644
--- a/spec/frontend/__helpers__/experimentation_helper.js
+++ b/spec/frontend/__helpers__/experimentation_helper.js
@@ -1,5 +1,6 @@
import { merge } from 'lodash';
+// This helper is for specs that use `gitlab/experimentation` module
export function withGonExperiment(experimentKey, value = true) {
let origGon;
@@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) {
window.gon = origGon;
});
}
-// This helper is for specs that use `gitlab-experiment` utilities, which have a different schema that gets pushed via Gon compared to `Experimentation Module`
-export function assignGitlabExperiment(experimentKey, variant) {
- let origGon;
- beforeEach(() => {
- origGon = window.gon;
- window.gon = { experiment: { [experimentKey]: { variant } } };
- });
+// The following helper is for specs that use `gitlab-experiment` utilities,
+// which have a different schema that gets pushed to the frontend compared to
+// the `Experimentation` Module.
+//
+// Usage: stubExperiments({ experiment_feature_flag_name: 'variant_name', ... })
+export function stubExperiments(experiments = {}) {
+ // Deprecated
+ window.gon = window.gon || {};
+ window.gon.experiment = window.gon.experiment || {};
+ // Preferred
+ window.gl = window.gl || {};
+ window.gl.experiments = window.gl.experiemnts || {};
- afterEach(() => {
- window.gon = origGon;
+ Object.entries(experiments).forEach(([name, variant]) => {
+ const experimentData = { experiment: name, variant };
+
+ // Deprecated
+ window.gon.experiment[name] = experimentData;
+ // Preferred
+ window.gl.experiments[name] = experimentData;
});
}
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 4c491a87fcb..6b3f1f01e6a 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -14,7 +14,9 @@ export * from '@gitlab/ui';
*/
jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
- bind() {},
+ GlTooltipDirective: {
+ bind() {},
+ },
}));
jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
index ee14e002f1b..c9a899ab78b 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
@@ -1,7 +1,7 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
-import { INTRO_COOKIE_KEY } from '~/analytics/devops_report/constants';
+import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
+import { INTRO_COOKIE_KEY } from '~/analytics/devops_reports/constants';
import * as utils from '~/lib/utils/common_utils';
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
index 8f8dac977de..824eb033671 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
@@ -2,8 +2,8 @@ import { GlTable, GlBadge, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
-import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
+import DevopsScore from '~/analytics/devops_reports/components/devops_score.vue';
+import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data';
describe('DevopsScore', () => {
diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js
new file mode 100644
index 00000000000..3b3be488043
--- /dev/null
+++ b/spec/frontend/admin/deploy_keys/components/table_spec.js
@@ -0,0 +1,47 @@
+import { merge } from 'lodash';
+import { GlTable, GlButton } from '@gitlab/ui';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
+
+describe('DeployKeysTable', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ createPath: '/admin/deploy_keys/new',
+ deletePath: '/admin/deploy_keys/:id',
+ editPath: '/admin/deploy_keys/:id/edit',
+ emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg',
+ };
+
+ const createComponent = (provide = {}) => {
+ wrapper = mountExtended(DeployKeysTable, {
+ provide: merge({}, defaultProvide, provide),
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders page title', () => {
+ createComponent();
+
+ expect(wrapper.findByText(DeployKeysTable.i18n.pageTitle).exists()).toBe(true);
+ });
+
+ it('renders table', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTable).exists()).toBe(true);
+ });
+
+ it('renders `New deploy key` button', () => {
+ createComponent();
+
+ const newDeployKeyButton = wrapper.findComponent(GlButton);
+
+ expect(newDeployKeyButton.text()).toBe(DeployKeysTable.i18n.newDeployKeyButtonText);
+ expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath);
+ });
+});
diff --git a/spec/frontend/alert_handler_spec.js b/spec/frontend/alert_handler_spec.js
index e4cd38a7799..228053b1b2b 100644
--- a/spec/frontend/alert_handler_spec.js
+++ b/spec/frontend/alert_handler_spec.js
@@ -26,12 +26,12 @@ describe('Alert Handler', () => {
});
it('should render the alert', () => {
- expect(findFirstAlert()).toExist();
+ expect(findFirstAlert()).not.toBe(null);
});
it('should dismiss the alert on click', () => {
findFirstDismissButton().click();
- expect(findFirstAlert()).not.toExist();
+ expect(findFirstAlert()).toBe(null);
});
});
@@ -58,12 +58,12 @@ describe('Alert Handler', () => {
});
it('should render the banner', () => {
- expect(findFirstBanner()).toExist();
+ expect(findFirstBanner()).not.toBe(null);
});
it('should dismiss the banner on click', () => {
findFirstDismissButton().click();
- expect(findFirstBanner()).not.toExist();
+ expect(findFirstBanner()).toBe(null);
});
});
@@ -79,12 +79,12 @@ describe('Alert Handler', () => {
});
it('should render the banner', () => {
- expect(findFirstAlert()).toExist();
+ expect(findFirstAlert()).not.toBe(null);
});
it('should dismiss the banner on click', () => {
findFirstDismissButtonByClass().click();
- expect(findFirstAlert()).not.toExist();
+ expect(findFirstAlert()).toBe(null);
});
});
});
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 20e8bc059ec..39aab8dc1f8 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -40,7 +40,6 @@ describe('AlertManagementTable', () => {
resolved: 11,
all: 26,
};
- const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
function mountComponent({ provide = {}, data = {}, loading = false, stubs = {} } = {}) {
wrapper = extendedWrapper(
@@ -49,7 +48,6 @@ describe('AlertManagementTable', () => {
...defaultProvideValues,
alertManagementEnabled: true,
userCanEnableAlertManagement: true,
- hasManagedPrometheus: false,
...provide,
},
data() {
@@ -237,22 +235,6 @@ describe('AlertManagementTable', () => {
expect(visitUrl).toHaveBeenCalledWith('/1527542/details', true);
});
- it.each`
- managedAlertsDeprecation | hasManagedPrometheus | isVisible
- ${false} | ${false} | ${false}
- ${false} | ${true} | ${true}
- ${true} | ${false} | ${false}
- ${true} | ${true} | ${false}
- `(
- 'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
- ({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
- mountComponent({
- provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } },
- });
- expect(findDeprecationNotice().exists()).toBe(isVisible);
- },
- );
-
describe('alert issue links', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 298596085ef..bdc1dde7d48 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -1,4 +1,12 @@
-import { GlForm, GlFormSelect, GlFormInput, GlToggle, GlFormTextarea, GlTab } from '@gitlab/ui';
+import {
+ GlForm,
+ GlFormSelect,
+ GlFormInput,
+ GlToggle,
+ GlFormTextarea,
+ GlTab,
+ GlLink,
+} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -58,7 +66,6 @@ describe('AlertsSettingsForm', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
@@ -69,7 +76,7 @@ describe('AlertsSettingsForm', () => {
const enableIntegration = (index, value) => {
findFormFields().at(index).setValue(value);
- findFormToggle().trigger('click');
+ findFormToggle().vm.$emit('change', true);
};
describe('with default values', () => {
@@ -102,6 +109,12 @@ describe('AlertsSettingsForm', () => {
expect(findFormFields().at(0).attributes('id')).not.toBe('name-integration');
});
+ it('verify pricing link url', () => {
+ createComponent({ props: { canAddIntegration: false } });
+ const link = findMultiSupportText().findComponent(GlLink);
+ expect(link.attributes('href')).toMatch(/https:\/\/about.gitlab.(com|cn)\/pricing/);
+ });
+
describe('form tabs', () => {
it('renders 3 tabs', () => {
expect(findTabs()).toHaveLength(3);
diff --git a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
index c5c40e9a360..c62bfb11f7b 100644
--- a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js
+++ b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
@@ -1,9 +1,9 @@
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ServicePingDisabled from '~/analytics/devops_report/components/service_ping_disabled.vue';
+import ServicePingDisabled from '~/analytics/devops_reports/components/service_ping_disabled.vue';
-describe('~/analytics/devops_report/components/service_ping_disabled.vue', () => {
+describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => {
let wrapper;
afterEach(() => {
diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
index 870375318e3..694c16a85c4 100644
--- a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
@@ -1,7 +1,6 @@
-import { within } from '@testing-library/dom';
-import { GlForm } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { GlForm, GlModal } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import ManageTwoFactorForm, {
i18n,
} from '~/authentication/two_factor_auth/components/manage_two_factor_form.vue';
@@ -17,100 +16,133 @@ describe('ManageTwoFactorForm', () => {
let wrapper;
const createComponent = (options = {}) => {
- wrapper = extendedWrapper(
- mount(ManageTwoFactorForm, {
- provide: {
- ...defaultProvide,
- webauthnEnabled: options?.webauthnEnabled ?? false,
- isCurrentPasswordRequired: options?.currentPasswordRequired ?? true,
- },
- }),
- );
+ wrapper = mountExtended(ManageTwoFactorForm, {
+ provide: {
+ ...defaultProvide,
+ webauthnEnabled: options?.webauthnEnabled ?? false,
+ isCurrentPasswordRequired: options?.currentPasswordRequired ?? true,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: `
+ <div>
+ <slot name="modal-title"></slot>
+ <slot></slot>
+ <slot name="modal-footer"></slot>
+ </div>`,
+ }),
+ },
+ });
};
- const queryByText = (text, options) => within(wrapper.element).queryByText(text, options);
- const queryByLabelText = (text, options) =>
- within(wrapper.element).queryByLabelText(text, options);
-
const findForm = () => wrapper.findComponent(GlForm);
const findMethodInput = () => wrapper.findByTestId('test-2fa-method-field');
const findDisableButton = () => wrapper.findByTestId('test-2fa-disable-button');
const findRegenerateCodesButton = () => wrapper.findByTestId('test-2fa-regenerate-codes-button');
+ const findConfirmationModal = () => wrapper.findComponent(GlModal);
+
+ const itShowsConfirmationModal = (confirmText) => {
+ it('shows confirmation modal', async () => {
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findDisableButton().trigger('click');
+
+ expect(findConfirmationModal().props('visible')).toBe(true);
+ expect(findConfirmationModal().html()).toContain(confirmText);
+ });
+ };
+
+ const itShowsValidationMessageIfCurrentPasswordFieldIsEmpty = (findButtonFunction) => {
+ it('shows validation message if `Current password` is empty', async () => {
+ await findButtonFunction().trigger('click');
+
+ expect(wrapper.findByText(i18n.currentPasswordInvalidFeedback).exists()).toBe(true);
+ });
+ };
beforeEach(() => {
createComponent();
});
- describe('Current password field', () => {
- it('renders the current password field', () => {
- expect(queryByLabelText(i18n.currentPassword).tagName).toEqual('INPUT');
+ describe('`Current password` field', () => {
+ describe('when required', () => {
+ it('renders the current password field', () => {
+ expect(wrapper.findByLabelText(i18n.currentPassword).exists()).toBe(true);
+ });
});
- });
- describe('when current password is not required', () => {
- beforeEach(() => {
- createComponent({
- currentPasswordRequired: false,
+ describe('when not required', () => {
+ beforeEach(() => {
+ createComponent({
+ currentPasswordRequired: false,
+ });
});
- });
- it('does not render the current password field', () => {
- expect(queryByLabelText(i18n.currentPassword)).toBe(null);
+ it('does not render the current password field', () => {
+ expect(wrapper.findByLabelText(i18n.currentPassword).exists()).toBe(false);
+ });
});
});
describe('Disable button', () => {
it('renders the component with correct attributes', () => {
expect(findDisableButton().exists()).toBe(true);
- expect(findDisableButton().attributes()).toMatchObject({
- 'data-confirm': i18n.confirm,
- 'data-form-action': defaultProvide.profileTwoFactorAuthPath,
- 'data-form-method': defaultProvide.profileTwoFactorAuthMethod,
- });
});
- it('has the right confirm text', () => {
- expect(findDisableButton().attributes('data-confirm')).toBe(i18n.confirm);
- });
+ describe('when clicked', () => {
+ itShowsValidationMessageIfCurrentPasswordFieldIsEmpty(findDisableButton);
- describe('when webauthnEnabled', () => {
- beforeEach(() => {
- createComponent({
- webauthnEnabled: true,
+ itShowsConfirmationModal(i18n.confirm);
+
+ describe('when webauthnEnabled', () => {
+ beforeEach(() => {
+ createComponent({
+ webauthnEnabled: true,
+ });
});
- });
- it('has the right confirm text', () => {
- expect(findDisableButton().attributes('data-confirm')).toBe(i18n.confirmWebAuthn);
+ itShowsConfirmationModal(i18n.confirmWebAuthn);
});
- });
- it('modifies the form action and method when submitted through the button', async () => {
- const form = findForm();
- const disableButton = findDisableButton().element;
- const methodInput = findMethodInput();
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = findForm();
+ const methodInput = findMethodInput();
+ const submitSpy = jest.spyOn(form.element, 'submit');
+
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findDisableButton().trigger('click');
+
+ expect(form.attributes('action')).toBe(defaultProvide.profileTwoFactorAuthPath);
+ expect(methodInput.attributes('value')).toBe(defaultProvide.profileTwoFactorAuthMethod);
- await form.vm.$emit('submit', { submitter: disableButton });
+ findConfirmationModal().vm.$emit('primary');
- expect(form.attributes('action')).toBe(defaultProvide.profileTwoFactorAuthPath);
- expect(methodInput.attributes('value')).toBe(defaultProvide.profileTwoFactorAuthMethod);
+ expect(submitSpy).toHaveBeenCalled();
+ });
});
});
describe('Regenerate recovery codes button', () => {
it('renders the button', () => {
- expect(queryByText(i18n.regenerateRecoveryCodes)).toEqual(expect.any(HTMLElement));
+ expect(findRegenerateCodesButton().exists()).toBe(true);
});
- it('modifies the form action and method when submitted through the button', async () => {
- const form = findForm();
- const regenerateCodesButton = findRegenerateCodesButton().element;
- const methodInput = findMethodInput();
+ describe('when clicked', () => {
+ itShowsValidationMessageIfCurrentPasswordFieldIsEmpty(findRegenerateCodesButton);
+
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = findForm();
+ const methodInput = findMethodInput();
+ const submitSpy = jest.spyOn(form.element, 'submit');
- await form.vm.$emit('submit', { submitter: regenerateCodesButton });
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findRegenerateCodesButton().trigger('click');
- expect(form.attributes('action')).toBe(defaultProvide.codesProfileTwoFactorAuthPath);
- expect(methodInput.attributes('value')).toBe(defaultProvide.codesProfileTwoFactorAuthMethod);
+ expect(form.attributes('action')).toBe(defaultProvide.codesProfileTwoFactorAuthPath);
+ expect(methodInput.attributes('value')).toBe(
+ defaultProvide.codesProfileTwoFactorAuthMethod,
+ );
+ expect(submitSpy).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index 41be04d0b7e..5327879f003 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -7,7 +7,7 @@ Vue.use(Vuex);
let wrapper;
-const toggleActiveFileByHash = jest.fn();
+const setCurrentFileHash = jest.fn();
const scrollToDraft = jest.fn();
function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts = [] } = {}) {
@@ -16,7 +16,7 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
diffs: {
namespaced: true,
actions: {
- toggleActiveFileByHash,
+ setCurrentFileHash,
},
state: {
viewDiffsFileByFile,
@@ -51,7 +51,7 @@ describe('Batch comments preview dropdown', () => {
await Vue.nextTick();
- expect(toggleActiveFileByHash).toHaveBeenCalledWith(expect.anything(), 'hash');
+ expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1, file_hash: 'hash' });
});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 286ed269421..d23a0a84997 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -56,13 +56,13 @@ describe('gl_emoji', () => {
'bomb emoji just with name attribute',
'<gl-emoji data-name="bomb"></gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/1/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with name attribute and unicode version',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/1/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with sprite fallback',
@@ -80,7 +80,7 @@ describe('gl_emoji', () => {
'invalid emoji',
'<gl-emoji data-name="invalid_emoji"></gl-emoji>',
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
- '<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/1/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
],
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
it(`renders correctly with emoji support`, async () => {
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
index 31fb6addcac..db9684239a1 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -4,9 +4,17 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
<div
class="js-file-title file-title-flex-parent"
>
- <blob-filepath-stub
- blob="[object Object]"
- />
+ <div
+ class="gl-display-flex"
+ >
+ <table-of-contents-stub
+ class="gl-pr-2"
+ />
+
+ <blob-filepath-stub
+ blob="[object Object]"
+ />
+ </div>
<div
class="gl-display-none gl-sm-display-flex"
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index f841785be42..bd81b1594bf 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -3,6 +3,7 @@ import BlobHeader from '~/blob/components/blob_header.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
+import TableContents from '~/blob/components/table_contents.vue';
import { Blob } from './mock_data';
@@ -43,6 +44,7 @@ describe('Blob Header Default Actions', () => {
it('renders all components', () => {
createComponent();
+ expect(wrapper.find(TableContents).exists()).toBe(true);
expect(wrapper.find(ViewerSwitcher).exists()).toBe(true);
expect(findDefaultActions().exists()).toBe(true);
expect(wrapper.find(BlobFilepath).exists()).toBe(true);
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 09633dc5d5d..ade35d39b4f 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -32,10 +32,30 @@ describe('Markdown table of contents component', () => {
});
describe('not loaded', () => {
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+
it('does not populate dropdown', () => {
createComponent();
- expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
+ expect(findDropdownItem().exists()).toBe(false);
+ });
+
+ it('does not show dropdown when loading blob content', async () => {
+ createComponent();
+
+ await setLoaded(false);
+
+ expect(findDropdownItem().exists()).toBe(false);
+ });
+
+ it('does not show dropdown when viewing non-rich content', async () => {
+ createComponent();
+
+ document.querySelector('.blob-viewer').setAttribute('data-type', 'simple');
+
+ await setLoaded(true);
+
+ expect(findDropdownItem().exists()).toBe(false);
});
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 25ec568e48d..5742dfdc5d2 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -64,12 +64,12 @@ describe('Board card', () => {
};
const selectCard = async () => {
- wrapper.trigger('mouseup');
+ wrapper.trigger('click');
await wrapper.vm.$nextTick();
};
const multiSelectCard = async () => {
- wrapper.trigger('mouseup', { ctrlKey: true });
+ wrapper.trigger('click', { ctrlKey: true });
await wrapper.vm.$nextTick();
};
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index dc93890f27a..b858d6e95a0 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -7,6 +7,7 @@ import { __ } from '~/locale';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+import { createStore } from '~/boards/stores';
Vue.use(Vuex);
@@ -42,17 +43,13 @@ describe('BoardFilteredSearch', () => {
},
];
- const createComponent = ({ initialFilterParams = {} } = {}) => {
- store = new Vuex.Store({
- actions: {
- performSearch: jest.fn(),
- },
- });
-
+ const createComponent = ({ initialFilterParams = {}, props = {} } = {}) => {
+ store = createStore();
wrapper = shallowMount(BoardFilteredSearch, {
provide: { initialFilterParams, fullPath: '' },
store,
propsData: {
+ ...props,
tokens,
},
});
@@ -68,11 +65,7 @@ describe('BoardFilteredSearch', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(store, 'dispatch');
- });
-
- it('renders FilteredSearch', () => {
- expect(findFilteredSearch().exists()).toBe(true);
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
it('passes the correct tokens to FilteredSearch', () => {
@@ -99,6 +92,22 @@ describe('BoardFilteredSearch', () => {
});
});
+ describe('when eeFilters is not empty', () => {
+ it('passes the correct initialFilterValue to FitleredSearchBarRoot', () => {
+ createComponent({ props: { eeFilters: { labelName: ['label'] } } });
+
+ expect(findFilteredSearch().props('initialFilterValue')).toEqual([
+ { type: 'label_name', value: { data: 'label', operator: '=' } },
+ ]);
+ });
+ });
+
+ it('renders FilteredSearch', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
describe('when searching', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 52f1907654a..692fd3ec555 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,7 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BoardForm from '~/boards/components/board_form.vue';
@@ -18,21 +17,18 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
const currentBoard = {
- id: 1,
+ id: 'gid://gitlab/Board/1',
name: 'test',
labels: [],
- milestone_id: undefined,
+ milestone: {},
assignee: {},
- assignee_id: undefined,
weight: null,
- hide_backlog_list: false,
- hide_closed_list: false,
+ hideBacklogList: false,
+ hideClosedList: false,
};
const defaultProps = {
canAdminBoard: false,
- labelsPath: `${TEST_HOST}/labels/path`,
- labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
currentPage: '',
};
@@ -252,7 +248,7 @@ describe('BoardForm', () => {
mutation: updateBoardMutation,
variables: {
input: expect.objectContaining({
- id: `gid://gitlab/Board/${currentBoard.id}`,
+ id: currentBoard.id,
}),
},
});
@@ -278,7 +274,7 @@ describe('BoardForm', () => {
mutation: updateBoardMutation,
variables: {
input: expect.objectContaining({
- id: `gid://gitlab/Board/${currentBoard.id}`,
+ id: currentBoard.id,
}),
},
});
@@ -326,7 +322,7 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalledWith({
mutation: destroyBoardMutation,
variables: {
- id: 'gid://gitlab/Board/1',
+ id: currentBoard.id,
},
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index bf317b51e83..c841c17a029 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,13 +1,22 @@
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
+import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
+import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
+import defaultStore from '~/boards/stores';
import axios from '~/lib/utils/axios_utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockGroupBoardResponse, mockProjectBoardResponse } from '../mock_data';
const throttleDuration = 1;
+Vue.use(VueApollo);
+
function boardGenerator(n) {
return new Array(n).fill().map((board, index) => {
const id = `${index}`;
@@ -25,9 +34,27 @@ describe('BoardsSelector', () => {
let allBoardsResponse;
let recentBoardsResponse;
let mock;
+ let fakeApollo;
+ let store;
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
+ const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
+ store = new Vuex.Store({
+ ...defaultStore,
+ actions: {
+ setError: jest.fn(),
+ },
+ getters: {
+ isGroupBoard: () => isGroupBoard,
+ isProjectBoard: () => isProjectBoard,
+ },
+ state: {
+ boardType: isGroupBoard ? 'group' : 'project',
+ },
+ });
+ };
+
const fillSearchBox = (filterTerm) => {
const searchBox = wrapper.find({ ref: 'searchBox' });
const searchBoxInput = searchBox.find('input');
@@ -40,52 +67,27 @@ describe('BoardsSelector', () => {
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findDropdown = () => wrapper.find(GlDropdown);
- beforeEach(() => {
- mock = new MockAdapter(axios);
- const $apollo = {
- queries: {
- boards: {
- loading: false,
- },
- },
- };
+ const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
+ const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
- allBoardsResponse = Promise.resolve({
- data: {
- group: {
- boards: {
- edges: boards.map((board) => ({ node: board })),
- },
- },
- },
- });
- recentBoardsResponse = Promise.resolve({
- data: recentBoards,
- });
+ const createComponent = () => {
+ fakeApollo = createMockApollo([
+ [projectBoardQuery, projectBoardQueryHandlerSuccess],
+ [groupBoardQuery, groupBoardQueryHandlerSuccess],
+ ]);
wrapper = mount(BoardsSelector, {
+ store,
+ apolloProvider: fakeApollo,
propsData: {
throttleDuration,
- currentBoard: {
- id: 1,
- name: 'Development',
- milestone_id: null,
- weight: null,
- assignee_id: null,
- labels: [],
- },
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,
weights: [],
},
- mocks: { $apollo },
attachTo: document.body,
provide: {
fullPath: '',
@@ -98,12 +100,7 @@ describe('BoardsSelector', () => {
[options.loadingKey]: true,
});
});
-
- mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
-
- // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- findDropdown().vm.$emit('show');
- });
+ };
afterEach(() => {
wrapper.destroy();
@@ -111,104 +108,158 @@ describe('BoardsSelector', () => {
mock.restore();
});
- describe('loading', () => {
- // we are testing loading state, so don't resolve responses until after the tests
- afterEach(() => {
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
- });
+ describe('fetching all boards', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
- it('shows loading spinner', () => {
- expect(getDropdownHeaders()).toHaveLength(0);
- expect(getDropdownItems()).toHaveLength(0);
- expect(getLoadingIcon().exists()).toBe(true);
+ allBoardsResponse = Promise.resolve({
+ data: {
+ group: {
+ boards: {
+ edges: boards.map((board) => ({ node: board })),
+ },
+ },
+ },
+ });
+ recentBoardsResponse = Promise.resolve({
+ data: recentBoards,
+ });
+
+ createStore();
+ createComponent();
+
+ mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
});
- });
- describe('loaded', () => {
- beforeEach(async () => {
- await wrapper.setData({
- loadingBoards: false,
+ describe('loading', () => {
+ beforeEach(async () => {
+ // Wait for current board to be loaded
+ await nextTick();
+
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
+ });
+
+ // we are testing loading state, so don't resolve responses until after the tests
+ afterEach(async () => {
+ await Promise.all([allBoardsResponse, recentBoardsResponse]);
+ await nextTick();
});
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
- });
- it('hides loading spinner', async () => {
- await wrapper.vm.$nextTick();
- expect(getLoadingIcon().exists()).toBe(false);
+ it('shows loading spinner', () => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(getLoadingIcon().exists()).toBe(true);
+ });
});
- describe('filtering', () => {
- beforeEach(() => {
- wrapper.setData({
- boards,
- });
+ describe('loaded', () => {
+ beforeEach(async () => {
+ // Wait for current board to be loaded
+ await nextTick();
+
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
- return nextTick();
+ await wrapper.setData({
+ loadingBoards: false,
+ loadingRecentBoards: false,
+ });
+ await Promise.all([allBoardsResponse, recentBoardsResponse]);
+ await nextTick();
});
- it('shows all boards without filtering', () => {
- expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ it('hides loading spinner', async () => {
+ await nextTick();
+ expect(getLoadingIcon().exists()).toBe(false);
});
- it('shows only matching boards when filtering', () => {
- const filterTerm = 'board1';
- const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
+ describe('filtering', () => {
+ beforeEach(async () => {
+ wrapper.setData({
+ boards,
+ });
+
+ await nextTick();
+ });
- fillSearchBox(filterTerm);
+ it('shows all boards without filtering', () => {
+ expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ });
- return nextTick().then(() => {
+ it('shows only matching boards when filtering', async () => {
+ const filterTerm = 'board1';
+ const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
+
+ fillSearchBox(filterTerm);
+
+ await nextTick();
expect(getDropdownItems()).toHaveLength(expectedCount);
});
- });
- it('shows message if there are no matching boards', () => {
- fillSearchBox('does not exist');
+ it('shows message if there are no matching boards', async () => {
+ fillSearchBox('does not exist');
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownItems()).toHaveLength(0);
expect(wrapper.text().includes('No matching boards found')).toBe(true);
});
});
- });
- describe('recent boards section', () => {
- it('shows only when boards are greater than 10', () => {
- wrapper.setData({
- boards,
- });
+ describe('recent boards section', () => {
+ it('shows only when boards are greater than 10', async () => {
+ wrapper.setData({
+ boards,
+ });
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(2);
});
- });
- it('does not show when boards are less than 10', () => {
- wrapper.setData({
- boards: boards.slice(0, 5),
- });
+ it('does not show when boards are less than 10', async () => {
+ wrapper.setData({
+ boards: boards.slice(0, 5),
+ });
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(0);
});
- });
- it('does not show when recentBoards api returns empty array', () => {
- wrapper.setData({
- recentBoards: [],
- });
+ it('does not show when recentBoards api returns empty array', async () => {
+ wrapper.setData({
+ recentBoards: [],
+ });
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(0);
});
- });
- it('does not show when search is active', () => {
- fillSearchBox('Random string');
+ it('does not show when search is active', async () => {
+ fillSearchBox('Random string');
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(0);
});
});
});
});
+
+ describe('fetching current board', () => {
+ it.each`
+ boardType | queryHandler | notCalledHandler
+ ${'group'} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${'project'} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ `('fetches $boardType board', async ({ boardType, queryHandler, notCalledHandler }) => {
+ createStore({
+ isProjectBoard: boardType === 'project',
+ isGroupBoard: boardType === 'group',
+ });
+ createComponent();
+
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index b6de46f8db8..45c5c87d800 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
+import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue';
import issueBoardFilters from '~/boards/issue_board_filters';
import { mockTokens } from '../mock_data';
@@ -9,39 +9,60 @@ jest.mock('~/boards/issue_board_filters');
describe('IssueBoardFilter', () => {
let wrapper;
- const createComponent = () => {
+ const findBoardsFilteredSearch = () => wrapper.findComponent(BoardFilteredSearch);
+
+ const createComponent = ({ isSignedIn = false } = {}) => {
wrapper = shallowMount(IssueBoardFilteredSpec, {
- props: { fullPath: '', boardType: '' },
+ propsData: { fullPath: 'gitlab-org', boardType: 'group' },
+ provide: {
+ isSignedIn,
+ },
});
};
+ let fetchAuthorsSpy;
+ let fetchLabelsSpy;
+ beforeEach(() => {
+ fetchAuthorsSpy = jest.fn();
+ fetchLabelsSpy = jest.fn();
+
+ issueBoardFilters.mockReturnValue({
+ fetchAuthors: fetchAuthorsSpy,
+ fetchLabels: fetchLabelsSpy,
+ });
+ });
+
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
- let fetchAuthorsSpy;
- let fetchLabelsSpy;
beforeEach(() => {
- fetchAuthorsSpy = jest.fn();
- fetchLabelsSpy = jest.fn();
-
- issueBoardFilters.mockReturnValue({
- fetchAuthors: fetchAuthorsSpy,
- fetchLabels: fetchLabelsSpy,
- });
-
createComponent();
});
it('finds BoardFilteredSearch', () => {
- expect(wrapper.find(BoardFilteredSearch).exists()).toBe(true);
+ expect(findBoardsFilteredSearch().exists()).toBe(true);
});
- it('passes the correct tokens to BoardFilteredSearch', () => {
- const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones);
+ it.each`
+ isSignedIn
+ ${true}
+ ${false}
+ `(
+ 'passes the correct tokens to BoardFilteredSearch when user sign in is $isSignedIn',
+ ({ isSignedIn }) => {
+ createComponent({ isSignedIn });
- expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens);
- });
+ const tokens = mockTokens(
+ fetchLabelsSpy,
+ fetchAuthorsSpy,
+ wrapper.vm.fetchMilestones,
+ isSignedIn,
+ );
+
+ expect(findBoardsFilteredSearch().props('tokens')).toEqual(tokens);
+ },
+ );
});
});
diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js
new file mode 100644
index 00000000000..075fe225ec2
--- /dev/null
+++ b/spec/frontend/boards/components/new_board_button_spec.js
@@ -0,0 +1,75 @@
+import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import NewBoardButton from '~/boards/components/new_board_button.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { stubExperiments } from 'helpers/experimentation_helper';
+import eventHub from '~/boards/eventhub';
+
+const FEATURE = 'prominent_create_board_btn';
+
+describe('NewBoardButton', () => {
+ let wrapper;
+
+ const createComponent = (args = {}) =>
+ extendedWrapper(
+ mount(NewBoardButton, {
+ provide: {
+ canAdminBoard: true,
+ multipleIssueBoardsAvailable: true,
+ ...args,
+ },
+ }),
+ );
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('control variant', () => {
+ beforeAll(() => {
+ stubExperiments({ [FEATURE]: 'control' });
+ });
+
+ it('renders nothing', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.text()).toBe('');
+ });
+ });
+
+ describe('candidate variant', () => {
+ beforeAll(() => {
+ stubExperiments({ [FEATURE]: 'candidate' });
+ });
+
+ it('renders New board button when `candidate` variant', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.text()).toBe('New board');
+ });
+
+ it('renders nothing when `canAdminBoard` is `false`', () => {
+ wrapper = createComponent({ canAdminBoard: false });
+
+ expect(wrapper.find(GlButton).exists()).toBe(false);
+ });
+
+ it('renders nothing when `multipleIssueBoardsAvailable` is `false`', () => {
+ wrapper = createComponent({ multipleIssueBoardsAvailable: false });
+
+ expect(wrapper.find(GlButton).exists()).toBe(false);
+ });
+
+ it('emits `showBoardModal` when button is clicked', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+
+ wrapper = createComponent();
+
+ wrapper.find(GlButton).vm.$emit('click', { preventDefault: () => {} });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBoardModal', 'new');
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
index 60474767f2d..fb9d823107e 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
@@ -105,6 +105,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
describe('when labels are updated over existing labels', () => {
const testLabelsPayload = [
{ id: 5, set: true },
+ { id: 6, set: false },
{ id: 7, set: true },
];
const expectedLabels = [{ id: 5 }, { id: 7 }];
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
index 8847f626c1f..6e1b528babc 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -14,8 +14,8 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
let store;
const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']");
- const findToggle = () => wrapper.find(GlToggle);
- const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createComponent = (activeBoardItem = { ...mockActiveIssue }) => {
store = createStore();
@@ -32,7 +32,6 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
afterEach(() => {
wrapper.destroy();
- wrapper = null;
store = null;
jest.clearAllMocks();
});
@@ -104,7 +103,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
expect(findGlLoadingIcon().exists()).toBe(false);
- findToggle().trigger('click');
+ findToggle().vm.$emit('change');
await wrapper.vm.$nextTick();
@@ -129,7 +128,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
expect(findGlLoadingIcon().exists()).toBe(false);
- findToggle().trigger('click');
+ findToggle().vm.$emit('change');
await wrapper.vm.$nextTick();
@@ -152,7 +151,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
- findToggle().trigger('click');
+ findToggle().vm.$emit('change');
await wrapper.vm.$nextTick();
expect(wrapper.vm.setError).toHaveBeenCalled();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 6a4f344bbfb..8fcad99f8a7 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -4,6 +4,7 @@ import { ListType } from '~/boards/constants';
import { __ } from '~/locale';
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_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';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
@@ -12,6 +13,7 @@ export const boardObj = {
id: 1,
name: 'test',
milestone_id: null,
+ labels: [],
};
export const listObj = {
@@ -29,17 +31,27 @@ export const listObj = {
},
};
-export const listObjDuplicate = {
- id: listObj.id,
- position: 1,
- title: 'Test',
- list_type: 'label',
- weight: 3,
- label: {
- id: listObj.label.id,
- title: 'Test',
- color: '#ff0000',
- description: 'testing;',
+export const mockGroupBoardResponse = {
+ data: {
+ workspace: {
+ board: {
+ id: 'gid://gitlab/Board/1',
+ name: 'Development',
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
+export const mockProjectBoardResponse = {
+ data: {
+ workspace: {
+ board: {
+ id: 'gid://gitlab/Board/2',
+ name: 'Development',
+ },
+ __typename: 'Project',
+ },
},
};
@@ -538,7 +550,16 @@ export const mockMoveData = {
...mockMoveIssueParams,
};
-export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
+export const mockEmojiToken = {
+ type: 'my_reaction_emoji',
+ icon: 'thumb-up',
+ title: 'My-Reaction',
+ unique: true,
+ token: EmojiToken,
+ fetchEmojis: expect.any(Function),
+};
+
+export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji) => [
{
icon: 'user',
title: __('Assignee'),
@@ -579,6 +600,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
symbol: '~',
fetchLabels,
},
+ ...(hasEmoji ? [mockEmojiToken] : []),
{
icon: 'clock',
title: __('Milestone'),
@@ -593,7 +615,6 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
icon: 'issues',
title: __('Type'),
type: 'types',
- operators: [{ value: '=', description: 'is' }],
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -609,3 +630,43 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
unique: true,
},
];
+
+export const mockLabel1 = {
+ id: 'gid://gitlab/GroupLabel/121',
+ title: 'To Do',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockLabel2 = {
+ id: 'gid://gitlab/GroupLabel/122',
+ title: 'Doing',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockProjectLabelsResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ labels: {
+ nodes: [mockLabel1, mockLabel2],
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockGroupLabelsResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/1',
+ labels: {
+ nodes: [mockLabel1, mockLabel2],
+ },
+ __typename: 'Group',
+ },
+ },
+};
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 0b90912a584..e245325b956 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -27,6 +27,7 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'
import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
mockLists,
@@ -1572,12 +1573,13 @@ describe('setActiveIssueLabels', () => {
const getters = { activeBoardItem: mockIssue };
const testLabelIds = labels.map((label) => label.id);
const input = {
- addLabelIds: testLabelIds,
+ labelIds: testLabelIds,
removeLabelIds: [],
projectPath: 'h/b',
+ labels,
};
- it('should assign labels on success, and sets loading state for labels', (done) => {
+ it('should assign labels on success', (done) => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
@@ -1594,14 +1596,6 @@ describe('setActiveIssueLabels', () => {
{ ...state, ...getters },
[
{
- type: types.SET_LABELS_LOADING,
- payload: true,
- },
- {
- type: types.SET_LABELS_LOADING,
- payload: false,
- },
- {
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
@@ -1618,6 +1612,64 @@ describe('setActiveIssueLabels', () => {
await expect(actions.setActiveIssueLabels({ getters }, input)).rejects.toThrow(Error);
});
+
+ describe('labels_widget FF on', () => {
+ beforeEach(() => {
+ window.gon = {
+ features: { labelsWidget: true },
+ };
+
+ getters.activeBoardItem = { ...mockIssue, labels };
+ });
+
+ afterEach(() => {
+ window.gon = {
+ features: {},
+ };
+ });
+
+ it('should assign labels', () => {
+ const payload = {
+ itemId: getters.activeBoardItem.id,
+ prop: 'labels',
+ value: labels,
+ };
+
+ testAction(
+ actions.setActiveIssueLabels,
+ input,
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ );
+ });
+
+ it('should remove label', () => {
+ const payload = {
+ itemId: getters.activeBoardItem.id,
+ prop: 'labels',
+ value: [labels[1]],
+ };
+
+ testAction(
+ actions.setActiveIssueLabels,
+ { ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ );
+ });
+ });
});
describe('setActiveItemSubscribed', () => {
diff --git a/spec/frontend/chronic_duration_spec.js b/spec/frontend/chronic_duration_spec.js
new file mode 100644
index 00000000000..32652e13dfc
--- /dev/null
+++ b/spec/frontend/chronic_duration_spec.js
@@ -0,0 +1,354 @@
+/*
+ * NOTE:
+ * Changes to this file should be kept in sync with
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration/-/blob/master/spec/lib/chronic_duration_spec.rb.
+ */
+
+/*
+ * This code is based on code from
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration and is
+ * distributed under the following license:
+ *
+ * MIT License
+ *
+ * Copyright (c) Henry Poydar
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import {
+ parseChronicDuration,
+ outputChronicDuration,
+ DurationParseError,
+} from '~/chronic_duration';
+
+describe('parseChronicDuration', () => {
+ /*
+ * TODO The Ruby implementation of this algorithm uses the Numerizer module,
+ * which converts strings like "forty two" to "42", but there is no
+ * JavaScript equivalent of Numerizer. Skip it for now until Numerizer is
+ * ported to JavaScript.
+ */
+ const EXEMPLARS = {
+ '1:20': 60 + 20,
+ '1:20.51': 60 + 20.51,
+ '4:01:01': 4 * 3600 + 60 + 1,
+ '3 mins 4 sec': 3 * 60 + 4,
+ '3 Mins 4 Sec': 3 * 60 + 4,
+ // 'three mins four sec': 3 * 60 + 4,
+ '2 hrs 20 min': 2 * 3600 + 20 * 60,
+ '2h20min': 2 * 3600 + 20 * 60,
+ '6 mos 1 day': 6 * 30 * 24 * 3600 + 24 * 3600,
+ '1 year 6 mos 1 day': 1 * 31557600 + 6 * 30 * 24 * 3600 + 24 * 3600,
+ '2.5 hrs': 2.5 * 3600,
+ '47 yrs 6 mos and 4.5d': 47 * 31557600 + 6 * 30 * 24 * 3600 + 4.5 * 24 * 3600,
+ // 'two hours and twenty minutes': 2 * 3600 + 20 * 60,
+ // 'four hours and forty minutes': 4 * 3600 + 40 * 60,
+ // 'four hours, and fourty minutes': 4 * 3600 + 40 * 60,
+ '3 weeks and, 2 days': 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
+ '3 weeks, plus 2 days': 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
+ '3 weeks with 2 days': 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
+ '1 month': 3600 * 24 * 30,
+ '2 months': 3600 * 24 * 30 * 2,
+ '18 months': 3600 * 24 * 30 * 18,
+ '1 year 6 months': 3600 * 24 * (365.25 + 6 * 30),
+ day: 3600 * 24,
+ 'minute 30s': 90,
+ };
+
+ describe("when string can't be parsed", () => {
+ it('returns null', () => {
+ expect(parseChronicDuration('gobblygoo')).toBeNull();
+ });
+
+ it('cannot parse zero', () => {
+ expect(parseChronicDuration('0')).toBeNull();
+ });
+
+ describe('when .raiseExceptions set to true', () => {
+ it('raises with DurationParseError', () => {
+ expect(() => parseChronicDuration('23 gobblygoos', { raiseExceptions: true })).toThrowError(
+ DurationParseError,
+ );
+ });
+
+ it('does not raise when string is empty', () => {
+ expect(parseChronicDuration('', { raiseExceptions: true })).toBeNull();
+ });
+ });
+ });
+
+ it('should return zero if the string parses as zero and the .keepZero option is true', () => {
+ expect(parseChronicDuration('0', { keepZero: true })).toBe(0);
+ });
+
+ it('should return a float if seconds are in decimals', () => {
+ expect(parseChronicDuration('12 mins 3.141 seconds')).toBeCloseTo(723.141, 4);
+ });
+
+ it('should return an integer unless the seconds are in decimals', () => {
+ expect(parseChronicDuration('12 mins 3 seconds')).toBe(723);
+ });
+
+ it('should be able to parse minutes by default', () => {
+ expect(parseChronicDuration('5', { defaultUnit: 'minutes' })).toBe(300);
+ });
+
+ Object.entries(EXEMPLARS).forEach(([k, v]) => {
+ it(`parses a duration like ${k}`, () => {
+ expect(parseChronicDuration(k)).toBe(v);
+ });
+ });
+
+ describe('with .hoursPerDay and .daysPerMonth params', () => {
+ it('uses provided .hoursPerDay', () => {
+ expect(parseChronicDuration('1d', { hoursPerDay: 24 })).toBe(24 * 60 * 60);
+ expect(parseChronicDuration('1d', { hoursPerDay: 8 })).toBe(8 * 60 * 60);
+ });
+
+ it('uses provided .daysPerMonth', () => {
+ expect(parseChronicDuration('1mo', { daysPerMonth: 30 })).toBe(30 * 24 * 60 * 60);
+ expect(parseChronicDuration('1mo', { daysPerMonth: 20 })).toBe(20 * 24 * 60 * 60);
+
+ expect(parseChronicDuration('1w', { daysPerMonth: 30 })).toBe(7 * 24 * 60 * 60);
+ expect(parseChronicDuration('1w', { daysPerMonth: 20 })).toBe(5 * 24 * 60 * 60);
+ });
+
+ it('uses provided both .hoursPerDay and .daysPerMonth', () => {
+ expect(parseChronicDuration('1mo', { daysPerMonth: 30, hoursPerDay: 24 })).toBe(
+ 30 * 24 * 60 * 60,
+ );
+ expect(parseChronicDuration('1mo', { daysPerMonth: 20, hoursPerDay: 8 })).toBe(
+ 20 * 8 * 60 * 60,
+ );
+
+ expect(parseChronicDuration('1w', { daysPerMonth: 30, hoursPerDay: 24 })).toBe(
+ 7 * 24 * 60 * 60,
+ );
+ expect(parseChronicDuration('1w', { daysPerMonth: 20, hoursPerDay: 8 })).toBe(
+ 5 * 8 * 60 * 60,
+ );
+ });
+ });
+});
+
+describe('outputChronicDuration', () => {
+ const EXEMPLARS = {
+ [60 + 20]: {
+ micro: '1m20s',
+ short: '1m 20s',
+ default: '1 min 20 secs',
+ long: '1 minute 20 seconds',
+ chrono: '1:20',
+ },
+ [60 + 20.51]: {
+ micro: '1m20.51s',
+ short: '1m 20.51s',
+ default: '1 min 20.51 secs',
+ long: '1 minute 20.51 seconds',
+ chrono: '1:20.51',
+ },
+ [60 + 20.51928]: {
+ micro: '1m20.51928s',
+ short: '1m 20.51928s',
+ default: '1 min 20.51928 secs',
+ long: '1 minute 20.51928 seconds',
+ chrono: '1:20.51928',
+ },
+ [4 * 3600 + 60 + 1]: {
+ micro: '4h1m1s',
+ short: '4h 1m 1s',
+ default: '4 hrs 1 min 1 sec',
+ long: '4 hours 1 minute 1 second',
+ chrono: '4:01:01',
+ },
+ [2 * 3600 + 20 * 60]: {
+ micro: '2h20m',
+ short: '2h 20m',
+ default: '2 hrs 20 mins',
+ long: '2 hours 20 minutes',
+ chrono: '2:20',
+ },
+ [2 * 3600 + 20 * 60]: {
+ micro: '2h20m',
+ short: '2h 20m',
+ default: '2 hrs 20 mins',
+ long: '2 hours 20 minutes',
+ chrono: '2:20:00',
+ },
+ [6 * 30 * 24 * 3600 + 24 * 3600]: {
+ micro: '6mo1d',
+ short: '6mo 1d',
+ default: '6 mos 1 day',
+ long: '6 months 1 day',
+ chrono: '6:01:00:00:00', // Yuck. FIXME
+ },
+ [365.25 * 24 * 3600 + 24 * 3600]: {
+ micro: '1y1d',
+ short: '1y 1d',
+ default: '1 yr 1 day',
+ long: '1 year 1 day',
+ chrono: '1:00:01:00:00:00',
+ },
+ [3 * 365.25 * 24 * 3600 + 24 * 3600]: {
+ micro: '3y1d',
+ short: '3y 1d',
+ default: '3 yrs 1 day',
+ long: '3 years 1 day',
+ chrono: '3:00:01:00:00:00',
+ },
+ [3600 * 24 * 30 * 18]: {
+ micro: '18mo',
+ short: '18mo',
+ default: '18 mos',
+ long: '18 months',
+ chrono: '18:00:00:00:00',
+ },
+ };
+
+ Object.entries(EXEMPLARS).forEach(([k, v]) => {
+ const kf = parseFloat(k);
+ Object.entries(v).forEach(([key, val]) => {
+ it(`properly outputs a duration of ${kf} seconds as ${val} using the ${key} format option`, () => {
+ expect(outputChronicDuration(kf, { format: key })).toBe(val);
+ });
+ });
+ });
+
+ const KEEP_ZERO_EXEMPLARS = {
+ true: {
+ micro: '0s',
+ short: '0s',
+ default: '0 secs',
+ long: '0 seconds',
+ chrono: '0',
+ },
+ '': {
+ micro: null,
+ short: null,
+ default: null,
+ long: null,
+ chrono: '0',
+ },
+ };
+
+ Object.entries(KEEP_ZERO_EXEMPLARS).forEach(([k, v]) => {
+ const kb = Boolean(k);
+ Object.entries(v).forEach(([key, val]) => {
+ it(`should properly output a duration of 0 seconds as ${val} using the ${key} format option, if the .keepZero option is ${kb}`, () => {
+ expect(outputChronicDuration(0, { format: key, keepZero: kb })).toBe(val);
+ });
+ });
+ });
+
+ it('returns weeks when needed', () => {
+ expect(outputChronicDuration(45 * 24 * 60 * 60, { weeks: true })).toMatch(/.*wk.*/);
+ });
+
+ it('returns hours and minutes only when .limitToHours option specified', () => {
+ expect(outputChronicDuration(395 * 24 * 60 * 60 + 15 * 60, { limitToHours: true })).toBe(
+ '9480 hrs 15 mins',
+ );
+ });
+
+ describe('with .hoursPerDay and .daysPerMonth params', () => {
+ it('uses provided .hoursPerDay', () => {
+ expect(outputChronicDuration(24 * 60 * 60, { hoursPerDay: 24 })).toBe('1 day');
+ expect(outputChronicDuration(24 * 60 * 60, { hoursPerDay: 8 })).toBe('3 days');
+ });
+
+ it('uses provided .daysPerMonth', () => {
+ expect(outputChronicDuration(7 * 24 * 60 * 60, { weeks: true, daysPerMonth: 30 })).toBe(
+ '1 wk',
+ );
+ expect(outputChronicDuration(7 * 24 * 60 * 60, { weeks: true, daysPerMonth: 20 })).toBe(
+ '1 wk 2 days',
+ );
+ });
+
+ it('uses provided both .hoursPerDay and .daysPerMonth', () => {
+ expect(
+ outputChronicDuration(7 * 24 * 60 * 60, { weeks: true, daysPerMonth: 30, hoursPerDay: 24 }),
+ ).toBe('1 wk');
+ expect(
+ outputChronicDuration(5 * 8 * 60 * 60, { weeks: true, daysPerMonth: 20, hoursPerDay: 8 }),
+ ).toBe('1 wk');
+ });
+
+ it('uses provided params alongside with .weeks when converting to months', () => {
+ expect(outputChronicDuration(30 * 24 * 60 * 60, { daysPerMonth: 30, hoursPerDay: 24 })).toBe(
+ '1 mo',
+ );
+ expect(
+ outputChronicDuration(30 * 24 * 60 * 60, {
+ daysPerMonth: 30,
+ hoursPerDay: 24,
+ weeks: true,
+ }),
+ ).toBe('1 mo 2 days');
+
+ expect(outputChronicDuration(20 * 8 * 60 * 60, { daysPerMonth: 20, hoursPerDay: 8 })).toBe(
+ '1 mo',
+ );
+ expect(
+ outputChronicDuration(20 * 8 * 60 * 60, { daysPerMonth: 20, hoursPerDay: 8, weeks: true }),
+ ).toBe('1 mo');
+ });
+ });
+
+ it('returns the specified number of units if provided', () => {
+ expect(outputChronicDuration(4 * 3600 + 60 + 1, { units: 2 })).toBe('4 hrs 1 min');
+ expect(
+ outputChronicDuration(6 * 30 * 24 * 3600 + 24 * 3600 + 3600 + 60 + 1, {
+ units: 3,
+ format: 'long',
+ }),
+ ).toBe('6 months 1 day 1 hour');
+ });
+
+ describe('when the format is not specified', () => {
+ it('uses the default format', () => {
+ expect(outputChronicDuration(2 * 3600 + 20 * 60)).toBe('2 hrs 20 mins');
+ });
+ });
+
+ Object.entries(EXEMPLARS).forEach(([seconds, formatSpec]) => {
+ const secondsF = parseFloat(seconds);
+ Object.keys(formatSpec).forEach((format) => {
+ it(`outputs a duration for ${seconds} that parses back to the same thing when using the ${format} format`, () => {
+ expect(parseChronicDuration(outputChronicDuration(secondsF, { format }))).toBe(secondsF);
+ });
+ });
+ });
+
+ it('uses user-specified joiner if provided', () => {
+ expect(outputChronicDuration(2 * 3600 + 20 * 60, { joiner: ', ' })).toBe('2 hrs, 20 mins');
+ });
+});
+
+describe('work week', () => {
+ it('should parse knowing the work week', () => {
+ const week = parseChronicDuration('5d', { hoursPerDay: 8, daysPerMonth: 20 });
+ expect(parseChronicDuration('40h', { hoursPerDay: 8, daysPerMonth: 20 })).toBe(week);
+ expect(parseChronicDuration('1w', { hoursPerDay: 8, daysPerMonth: 20 })).toBe(week);
+ });
+});
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index fd04ff8b3e7..c502e7d813e 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -1,6 +1,8 @@
import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlSprintf, GlTab } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ClusterAgentShow from '~/clusters/agents/components/show.vue';
import TokenTable from '~/clusters/agents/components/token_table.vue';
import getAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
@@ -40,28 +42,34 @@ describe('ClusterAgentShow', () => {
queryResponse || jest.fn().mockResolvedValue({ data: { project: { clusterAgent } } });
const apolloProvider = createMockApollo([[getAgentQuery, agentQueryResponse]]);
- wrapper = shallowMount(ClusterAgentShow, {
- localVue,
- apolloProvider,
- propsData,
- stubs: { GlSprintf, TimeAgoTooltip, GlTab },
- });
+ wrapper = extendedWrapper(
+ shallowMount(ClusterAgentShow, {
+ localVue,
+ apolloProvider,
+ propsData,
+ stubs: { GlSprintf, TimeAgoTooltip, GlTab },
+ }),
+ );
};
- const createWrapperWithoutApollo = ({ clusterAgent, loading = false }) => {
+ const createWrapperWithoutApollo = ({ clusterAgent, loading = false, slots = {} }) => {
const $apollo = { queries: { clusterAgent: { loading } } };
- wrapper = shallowMount(ClusterAgentShow, {
- propsData,
- mocks: { $apollo, clusterAgent },
- stubs: { GlTab },
- });
+ wrapper = extendedWrapper(
+ shallowMount(ClusterAgentShow, {
+ propsData,
+ mocks: { $apollo, clusterAgent },
+ slots,
+ stubs: { GlTab },
+ }),
+ );
};
- const findCreatedText = () => wrapper.find('[data-testid="cluster-agent-create-info"]').text();
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findPaginationButtons = () => wrapper.find(GlKeysetPagination);
- const findTokenCount = () => wrapper.find('[data-testid="cluster-agent-token-count"]').text();
+ const findCreatedText = () => wrapper.findByTestId('cluster-agent-create-info').text();
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPaginationButtons = () => wrapper.findComponent(GlKeysetPagination);
+ const findTokenCount = () => wrapper.findByTestId('cluster-agent-token-count').text();
+ const findEESecurityTabSlot = () => wrapper.findByTestId('ee-security-tab');
afterEach(() => {
wrapper.destroy();
@@ -87,7 +95,7 @@ describe('ClusterAgentShow', () => {
});
it('renders token table', () => {
- expect(wrapper.find(TokenTable).exists()).toBe(true);
+ expect(wrapper.findComponent(TokenTable).exists()).toBe(true);
});
it('should not render pagination buttons when there are no additional pages', () => {
@@ -188,8 +196,27 @@ describe('ClusterAgentShow', () => {
});
it('displays an alert message', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
expect(wrapper.text()).toContain(ClusterAgentShow.i18n.loadingError);
});
});
+
+ describe('ee-security-tab slot', () => {
+ it('does not display when a slot is not passed in', async () => {
+ createWrapperWithoutApollo({ clusterAgent: defaultClusterAgent });
+ await nextTick();
+ expect(findEESecurityTabSlot().exists()).toBe(false);
+ });
+
+ it('does display when a slot is passed in', async () => {
+ createWrapperWithoutApollo({
+ clusterAgent: defaultClusterAgent,
+ slots: {
+ 'ee-security-tab': `<gl-tab data-testid="ee-security-tab">Security Tab!</gl-tab>`,
+ },
+ });
+ await nextTick();
+ expect(findEESecurityTabSlot().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
index e2726b93ea5..41bd492148e 100644
--- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -1,18 +1,20 @@
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue';
import SplitButton from '~/vue_shared/components/split_button.vue';
describe('Remove cluster confirmation modal', () => {
let wrapper;
- const createComponent = (props = {}) => {
+ const createComponent = ({ props = {}, stubs = {} } = {}) => {
wrapper = mount(RemoveClusterConfirmation, {
propsData: {
clusterPath: 'clusterPath',
clusterName: 'clusterName',
...props,
},
+ stubs,
});
};
@@ -27,35 +29,44 @@ describe('Remove cluster confirmation modal', () => {
});
describe('split button dropdown', () => {
- const findModal = () => wrapper.find(GlModal).vm;
- const findSplitButton = () => wrapper.find(SplitButton);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findSplitButton = () => wrapper.findComponent(SplitButton);
beforeEach(() => {
- createComponent({ clusterName: 'my-test-cluster' });
- jest.spyOn(findModal(), 'show').mockReturnValue();
+ createComponent({
+ props: { clusterName: 'my-test-cluster' },
+ stubs: { GlSprintf, GlModal: stubComponent(GlModal) },
+ });
+ jest.spyOn(findModal().vm, 'show').mockReturnValue();
});
- it('opens modal with "cleanup" option', () => {
+ it('opens modal with "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster-and-cleanup');
- return wrapper.vm.$nextTick().then(() => {
- expect(findModal().show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(true);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findModal().vm.show).toHaveBeenCalled();
+ expect(wrapper.vm.confirmCleanup).toEqual(true);
+ expect(findModal().html()).toContain(
+ '<strong>To remove your integration and resources, type <code>my-test-cluster</code> to confirm:</strong>',
+ );
});
- it('opens modal without "cleanup" option', () => {
+ it('opens modal without "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster');
- return wrapper.vm.$nextTick().then(() => {
- expect(findModal().show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(false);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findModal().vm.show).toHaveBeenCalled();
+ expect(wrapper.vm.confirmCleanup).toEqual(false);
+ expect(findModal().html()).toContain(
+ '<strong>To remove your integration, type <code>my-test-cluster</code> to confirm:</strong>',
+ );
});
describe('with cluster management project', () => {
beforeEach(() => {
- createComponent({ hasManagementProject: true });
+ createComponent({ props: { hasManagementProject: true } });
});
it('renders regular button instead', () => {
diff --git a/spec/frontend/clusters_list/components/agent_empty_state_spec.js b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
index a548721588e..38f0e0ba2c4 100644
--- a/spec/frontend/clusters_list/components/agent_empty_state_spec.js
+++ b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
@@ -1,13 +1,12 @@
import { GlAlert, GlEmptyState, GlSprintf } from '@gitlab/ui';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image';
const projectPath = 'path/to/project';
-const agentDocsUrl = 'path/to/agentDocs';
-const installDocsUrl = 'path/to/installDocs';
-const getStartedDocsUrl = 'path/to/getStartedDocs';
-const integrationDocsUrl = 'path/to/integrationDocs';
+const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters');
+const installDocsUrl = helpPagePath('administration/clusters/kas');
describe('AgentEmptyStateComponent', () => {
let wrapper;
@@ -18,14 +17,10 @@ describe('AgentEmptyStateComponent', () => {
const provideData = {
emptyStateImage,
projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
};
const findConfigurationsAlert = () => wrapper.findComponent(GlAlert);
- const findAgentDocsLink = () => wrapper.findByTestId('agent-docs-link');
+ const findMultipleClustersDocsLink = () => wrapper.findByTestId('multiple-clusters-docs-link');
const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link');
const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button');
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -41,12 +36,11 @@ describe('AgentEmptyStateComponent', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
it('renders correct href attributes for the links', () => {
- expect(findAgentDocsLink().attributes('href')).toBe(agentDocsUrl);
+ expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl);
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index e3b90584f29..a6d76b069cf 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
+import { GlLink, GlIcon } from '@gitlab/ui';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -47,7 +47,6 @@ const propsData = {
},
],
};
-const provideData = { integrationDocsUrl: 'path/to/integrationDocs' };
describe('AgentTable', () => {
let wrapper;
@@ -60,7 +59,7 @@ describe('AgentTable', () => {
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
beforeEach(() => {
- wrapper = mountExtended(AgentTable, { propsData, provide: provideData });
+ wrapper = mountExtended(AgentTable, { propsData });
});
afterEach(() => {
@@ -70,10 +69,6 @@ describe('AgentTable', () => {
}
});
- it('displays header button', () => {
- expect(wrapper.find(GlButton).text()).toBe('Install a new GitLab Agent');
- });
-
describe('agent table', () => {
it.each`
agentName | link | lineNumber
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index 54d5ae94172..2dec7cdc973 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -14,7 +14,7 @@ localVue.use(VueApollo);
describe('Agents', () => {
let wrapper;
- const propsData = {
+ const defaultProps = {
defaultBranchName: 'default',
};
const provideData = {
@@ -22,12 +22,12 @@ describe('Agents', () => {
kasAddress: 'kas.example.com',
};
- const createWrapper = ({ agents = [], pageInfo = null, trees = [] }) => {
+ const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => {
const provide = provideData;
const apolloQueryResponse = {
data: {
project: {
- clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] } },
+ clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] }, count },
repository: { tree: { trees: { nodes: trees, pageInfo } } },
},
},
@@ -40,7 +40,10 @@ describe('Agents', () => {
wrapper = shallowMount(Agents, {
localVue,
apolloProvider,
- propsData,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
provide: provideData,
});
@@ -54,7 +57,6 @@ describe('Agents', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
@@ -81,6 +83,8 @@ describe('Agents', () => {
},
];
+ const count = 2;
+
const trees = [
{
name: 'agent-2',
@@ -121,7 +125,7 @@ describe('Agents', () => {
];
beforeEach(() => {
- return createWrapper({ agents, trees });
+ return createWrapper({ agents, count, trees });
});
it('should render agent table', () => {
@@ -133,6 +137,10 @@ describe('Agents', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
});
+ it('should emit agents count to the parent component', () => {
+ expect(wrapper.emitted().onAgentsLoad).toEqual([[count]]);
+ });
+
describe('when the agent has recently connected tokens', () => {
it('should set agent status to active', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
@@ -180,6 +188,20 @@ describe('Agents', () => {
it('should pass pageInfo to the pagination component', () => {
expect(findPaginationButtons().props()).toMatchObject(pageInfo);
});
+
+ describe('when limit is passed from the parent component', () => {
+ beforeEach(() => {
+ return createWrapper({
+ props: { limit: 6 },
+ agents,
+ pageInfo,
+ });
+ });
+
+ it('should not render pagination buttons', () => {
+ expect(findPaginationButtons().exists()).toBe(false);
+ });
+ });
});
});
@@ -234,7 +256,11 @@ describe('Agents', () => {
};
beforeEach(() => {
- wrapper = shallowMount(Agents, { mocks, propsData, provide: provideData });
+ wrapper = shallowMount(Agents, {
+ mocks,
+ propsData: defaultProps,
+ provide: provideData,
+ });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
new file mode 100644
index 00000000000..cb8303ca4b2
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -0,0 +1,55 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '~/clusters_list/constants';
+
+describe('ClustersActionsComponent', () => {
+ let wrapper;
+
+ const newClusterPath = 'path/to/create/cluster';
+ const addClusterPath = 'path/to/connect/existing/cluster';
+
+ const provideData = {
+ newClusterPath,
+ addClusterPath,
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
+ const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
+ const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersActions, {
+ provide: provideData,
+ directives: {
+ GlModalDirective: createMockDirective(),
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders actions menu', () => {
+ expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
+ });
+
+ it('renders a dropdown with 3 actions items', () => {
+ expect(findDropdownItems()).toHaveLength(3);
+ });
+
+ it('renders correct href attributes for the links', () => {
+ expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
+ expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
+ });
+
+ it('renders correct modal id for the agent link', () => {
+ const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
+
+ expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
+ });
+});
diff --git a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
new file mode 100644
index 00000000000..f7e1791d0f7
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
@@ -0,0 +1,104 @@
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
+import ClusterStore from '~/clusters_list/store';
+
+const clustersEmptyStateImage = 'path/to/svg';
+const newClusterPath = '/path/to/connect/cluster';
+const emptyStateHelpText = 'empty state text';
+const canAddCluster = true;
+
+describe('ClustersEmptyStateComponent', () => {
+ let wrapper;
+
+ const propsData = {
+ isChildComponent: false,
+ };
+
+ const provideData = {
+ clustersEmptyStateImage,
+ emptyStateHelpText: null,
+ newClusterPath,
+ };
+
+ const entryData = {
+ canAddCluster,
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findEmptyStateText = () => wrapper.findByTestId('clusters-empty-state-text');
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ stubs: { GlEmptyState },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when the component is loaded independently', () => {
+ it('should render the action button', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when the help text is not provided', () => {
+ it('should not render the empty state text', () => {
+ expect(findEmptyStateText().exists()).toBe(false);
+ });
+ });
+
+ describe('when the component is loaded as a child component', () => {
+ beforeEach(() => {
+ propsData.isChildComponent = true;
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ });
+ });
+
+ afterEach(() => {
+ propsData.isChildComponent = false;
+ });
+
+ it('should not render the action button', () => {
+ expect(findButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when the help text is provided', () => {
+ beforeEach(() => {
+ provideData.emptyStateHelpText = emptyStateHelpText;
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ });
+ });
+
+ it('should show the empty state text', () => {
+ expect(findEmptyStateText().text()).toBe(emptyStateHelpText);
+ });
+ });
+
+ describe('when the user cannot add clusters', () => {
+ entryData.canAddCluster = false;
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ stubs: { GlEmptyState },
+ });
+ });
+ it('should disable the button', () => {
+ expect(findButton().props('disabled')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/clusters_main_view_spec.js b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
new file mode 100644
index 00000000000..c2233e5d39c
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
@@ -0,0 +1,82 @@
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersMainView from '~/clusters_list/components/clusters_main_view.vue';
+import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue';
+import {
+ AGENT,
+ CERTIFICATE_BASED,
+ CLUSTERS_TABS,
+ MAX_CLUSTERS_LIST,
+ MAX_LIST_COUNT,
+} from '~/clusters_list/constants';
+
+const defaultBranchName = 'default-branch';
+
+describe('ClustersMainViewComponent', () => {
+ let wrapper;
+
+ const propsData = {
+ defaultBranchName,
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersMainView, {
+ propsData,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findAllTabs = () => wrapper.findAllComponents(GlTab);
+ const findGlTabAtIndex = (index) => findAllTabs().at(index);
+ const findComponent = () => wrapper.findByTestId('clusters-tab-component');
+ const findModal = () => wrapper.findComponent(InstallAgentModal);
+
+ it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
+ expect(findTabs().exists()).toBe(true);
+ expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
+ });
+
+ it('renders correct number of tabs', () => {
+ expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
+ });
+
+ it('passes child-component param to the component', () => {
+ expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
+ });
+
+ it('passes correct max-agents param to the modal', () => {
+ expect(findModal().props('maxAgents')).toBe(MAX_CLUSTERS_LIST);
+ });
+
+ describe('tabs', () => {
+ it.each`
+ tabTitle | queryParamValue | lineNumber
+ ${'All'} | ${'all'} | ${0}
+ ${'Agent'} | ${AGENT} | ${1}
+ ${'Certificate based'} | ${CERTIFICATE_BASED} | ${2}
+ `(
+ 'renders correct tab title and query param value',
+ ({ tabTitle, queryParamValue, lineNumber }) => {
+ expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
+ expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
+ },
+ );
+ });
+
+ describe('when the child component emits the tab change event', () => {
+ beforeEach(() => {
+ findComponent().vm.$emit('changeTab', AGENT);
+ });
+ it('changes the tab', () => {
+ expect(findTabs().attributes('value')).toBe('1');
+ });
+
+ it('passes correct max-agents param to the modal', () => {
+ expect(findModal().props('maxAgents')).toBe(MAX_LIST_COUNT);
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 941a3adb625..a34202c789d 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -8,6 +8,7 @@ import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Clusters from '~/clusters_list/components/clusters.vue';
+import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
import axios from '~/lib/utils/axios_utils';
import { apiData } from '../mock_data';
@@ -18,26 +19,38 @@ describe('Clusters', () => {
let wrapper;
const endpoint = 'some/endpoint';
+ const totalClustersNumber = 6;
+ const clustersEmptyStateImage = 'path/to/svg';
+ const emptyStateHelpText = null;
+ const newClusterPath = '/path/to/new/cluster';
const entryData = {
endpoint,
imgTagsAwsText: 'AWS Icon',
imgTagsDefaultText: 'Default Icon',
imgTagsGcpText: 'GCP Icon',
+ totalClusters: totalClustersNumber,
};
- const findLoader = () => wrapper.find(GlLoadingIcon);
- const findPaginatedButtons = () => wrapper.find(GlPagination);
- const findTable = () => wrapper.find(GlTable);
+ const provideData = {
+ clustersEmptyStateImage,
+ emptyStateHelpText,
+ newClusterPath,
+ };
+
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
+ const findPaginatedButtons = () => wrapper.findComponent(GlPagination);
+ const findTable = () => wrapper.findComponent(GlTable);
const findStatuses = () => findTable().findAll('.js-status');
+ const findEmptyState = () => wrapper.findComponent(ClustersEmptyState);
const mockPollingApi = (response, body, header) => {
mock.onGet(`${endpoint}?page=${header['x-page']}`).reply(response, body, header);
};
- const mountWrapper = () => {
+ const createWrapper = ({ propsData = {} }) => {
store = ClusterStore(entryData);
- wrapper = mount(Clusters, { store });
+ wrapper = mount(Clusters, { propsData, provide: provideData, store, stubs: { GlTable } });
return axios.waitForAll();
};
@@ -57,7 +70,7 @@ describe('Clusters', () => {
mock = new MockAdapter(axios);
mockPollingApi(200, apiData, paginationHeader());
- return mountWrapper();
+ return createWrapper({});
});
afterEach(() => {
@@ -70,7 +83,6 @@ describe('Clusters', () => {
describe('when data is loading', () => {
beforeEach(() => {
wrapper.vm.$store.state.loadingClusters = true;
- return wrapper.vm.$nextTick();
});
it('displays a loader instead of the table while loading', () => {
@@ -79,23 +91,29 @@ describe('Clusters', () => {
});
});
- it('displays a table component', () => {
- expect(findTable().exists()).toBe(true);
+ describe('when clusters are present', () => {
+ it('displays a table component', () => {
+ expect(findTable().exists()).toBe(true);
+ });
});
- it('renders the correct table headers', () => {
- const tableHeaders = wrapper.vm.fields;
- const headers = findTable().findAll('th');
-
- expect(headers.length).toBe(tableHeaders.length);
-
- tableHeaders.forEach((headerText, i) =>
- expect(headers.at(i).text()).toEqual(headerText.label),
- );
+ describe('when there are no clusters', () => {
+ beforeEach(() => {
+ wrapper.vm.$store.state.totalClusters = 0;
+ });
+ it('should render empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
});
- it('should stack on smaller devices', () => {
- expect(findTable().classes()).toContain('b-table-stacked-md');
+ describe('when is loaded as a child component', () => {
+ beforeEach(() => {
+ createWrapper({ limit: 6 });
+ });
+
+ it("shouldn't render pagination buttons", () => {
+ expect(findPaginatedButtons().exists()).toBe(false);
+ });
});
});
@@ -240,7 +258,7 @@ describe('Clusters', () => {
beforeEach(() => {
mockPollingApi(200, apiData, paginationHeader(totalFirstPage, perPage, 1));
- return mountWrapper();
+ return createWrapper({});
});
it('should load to page 1 with header values', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_view_all_spec.js b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
new file mode 100644
index 00000000000..6ef56beddee
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
@@ -0,0 +1,243 @@
+import { GlCard, GlLoadingIcon, GlButton, GlSprintf, GlBadge } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersViewAll from '~/clusters_list/components/clusters_view_all.vue';
+import Agents from '~/clusters_list/components/agents.vue';
+import Clusters from '~/clusters_list/components/clusters.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import {
+ AGENT,
+ CERTIFICATE_BASED,
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+} from '~/clusters_list/constants';
+import { sprintf } from '~/locale';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const addClusterPath = '/path/to/add/cluster';
+const defaultBranchName = 'default-branch';
+
+describe('ClustersViewAllComponent', () => {
+ let wrapper;
+
+ const event = {
+ preventDefault: jest.fn(),
+ };
+
+ const propsData = {
+ defaultBranchName,
+ };
+
+ const provideData = {
+ addClusterPath,
+ };
+
+ const entryData = {
+ loadingClusters: false,
+ totalClusters: 0,
+ };
+
+ const findCards = () => wrapper.findAllComponents(GlCard);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAgentsComponent = () => wrapper.findComponent(Agents);
+ const findClustersComponent = () => wrapper.findComponent(Clusters);
+ const findCardsContainer = () => wrapper.findByTestId('clusters-cards-container');
+ const findAgentCardTitle = () => wrapper.findByTestId('agent-card-title');
+ const findRecommendedBadge = () => wrapper.findComponent(GlBadge);
+ const findClustersCardTitle = () => wrapper.findByTestId('clusters-card-title');
+ const findFooterButton = (line) => findCards().at(line).findComponent(GlButton);
+
+ const createStore = (initialState) =>
+ new Vuex.Store({
+ state: initialState,
+ });
+
+ const createWrapper = ({ initialState }) => {
+ wrapper = shallowMountExtended(ClustersViewAll, {
+ localVue,
+ store: createStore(initialState),
+ propsData,
+ provide: provideData,
+ directives: {
+ GlModalDirective: createMockDirective(),
+ },
+ stubs: { GlCard, GlSprintf },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper({ initialState: entryData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when agents and clusters are not loaded', () => {
+ const initialState = {
+ loadingClusters: true,
+ totalClusters: 0,
+ };
+ beforeEach(() => {
+ createWrapper({ initialState });
+ });
+
+ it('should show the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when both agents and clusters are loaded', () => {
+ beforeEach(() => {
+ findAgentsComponent().vm.$emit('onAgentsLoad', 6);
+ });
+
+ it("shouldn't show the loading icon", () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('should make content visible', () => {
+ expect(findCardsContainer().isVisible()).toBe(true);
+ });
+
+ it('should render 2 cards', () => {
+ expect(findCards().length).toBe(2);
+ });
+ });
+
+ describe('agents card', () => {
+ it('should show recommended badge', () => {
+ expect(findRecommendedBadge().exists()).toBe(true);
+ });
+
+ it('should render Agents component', () => {
+ expect(findAgentsComponent().exists()).toBe(true);
+ });
+
+ it('should pass the limit prop', () => {
+ expect(findAgentsComponent().props('limit')).toBe(MAX_CLUSTERS_LIST);
+ });
+
+ it('should pass the default-branch-name prop', () => {
+ expect(findAgentsComponent().props('defaultBranchName')).toBe(defaultBranchName);
+ });
+
+ describe('when there are no agents', () => {
+ it('should show the empty title', () => {
+ expect(findAgentCardTitle().text()).toBe(AGENT_CARD_INFO.emptyTitle);
+ });
+
+ it('should show install new Agent button in the footer', () => {
+ expect(findFooterButton(0).exists()).toBe(true);
+ });
+
+ it('should render correct modal id for the agent link', () => {
+ const binding = getBinding(findFooterButton(0).element, 'gl-modal-directive');
+
+ expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
+ });
+ });
+
+ describe('when the agents are present', () => {
+ const findFooterLink = () => wrapper.findByTestId('agents-tab-footer-link');
+ const agentsNumber = 7;
+
+ beforeEach(() => {
+ findAgentsComponent().vm.$emit('onAgentsLoad', agentsNumber);
+ });
+
+ it('should show the correct title', () => {
+ expect(findAgentCardTitle().text()).toBe(
+ sprintf(AGENT_CARD_INFO.title, { number: MAX_CLUSTERS_LIST, total: agentsNumber }),
+ );
+ });
+
+ it('should show the link to the Agents tab in the footer', () => {
+ expect(findFooterLink().exists()).toBe(true);
+ expect(findFooterLink().text()).toBe(
+ sprintf(AGENT_CARD_INFO.footerText, { number: agentsNumber }),
+ );
+ expect(findFooterLink().attributes('href')).toBe(`?tab=${AGENT}`);
+ });
+
+ describe('when clicking on the footer link', () => {
+ beforeEach(() => {
+ findFooterLink().vm.$emit('click', event);
+ });
+
+ it('should trigger tab change', () => {
+ expect(wrapper.emitted('changeTab')).toEqual([[AGENT]]);
+ });
+ });
+ });
+ });
+
+ describe('clusters tab', () => {
+ it('should pass the limit prop', () => {
+ expect(findClustersComponent().props('limit')).toBe(MAX_CLUSTERS_LIST);
+ });
+
+ it('should pass the is-child-component prop', () => {
+ expect(findClustersComponent().props('isChildComponent')).toBe(true);
+ });
+
+ describe('when there are no clusters', () => {
+ it('should show the empty title', () => {
+ expect(findClustersCardTitle().text()).toBe(CERTIFICATE_BASED_CARD_INFO.emptyTitle);
+ });
+
+ it('should show install new Agent button in the footer', () => {
+ expect(findFooterButton(1).exists()).toBe(true);
+ });
+
+ it('should render correct href for the button in the footer', () => {
+ expect(findFooterButton(1).attributes('href')).toBe(addClusterPath);
+ });
+ });
+
+ describe('when the clusters are present', () => {
+ const findFooterLink = () => wrapper.findByTestId('clusters-tab-footer-link');
+
+ const clustersNumber = 7;
+ const initialState = {
+ loadingClusters: false,
+ totalClusters: clustersNumber,
+ };
+
+ beforeEach(() => {
+ createWrapper({ initialState });
+ });
+
+ it('should show the correct title', () => {
+ expect(findClustersCardTitle().text()).toBe(
+ sprintf(CERTIFICATE_BASED_CARD_INFO.title, {
+ number: MAX_CLUSTERS_LIST,
+ total: clustersNumber,
+ }),
+ );
+ });
+
+ it('should show the link to the Clusters tab in the footer', () => {
+ expect(findFooterLink().exists()).toBe(true);
+ expect(findFooterLink().text()).toBe(
+ sprintf(CERTIFICATE_BASED_CARD_INFO.footerText, { number: clustersNumber }),
+ );
+ });
+
+ describe('when clicking on the footer link', () => {
+ beforeEach(() => {
+ findFooterLink().vm.$emit('click', event);
+ });
+
+ it('should trigger tab change', () => {
+ expect(wrapper.emitted('changeTab')).toEqual([[CERTIFICATE_BASED]]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 98ca5e05b3f..6c2ea45b99b 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -3,7 +3,8 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue';
-import { I18N_INSTALL_AGENT_MODAL } from '~/clusters_list/constants';
+import { I18N_INSTALL_AGENT_MODAL, MAX_LIST_COUNT } from '~/clusters_list/constants';
+import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import createAgentMutation from '~/clusters_list/graphql/mutations/create_agent.mutation.graphql';
import createAgentTokenMutation from '~/clusters_list/graphql/mutations/create_agent_token.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -14,12 +15,17 @@ import {
createAgentErrorResponse,
createAgentTokenResponse,
createAgentTokenErrorResponse,
+ getAgentResponse,
} from '../mocks/apollo';
import ModalStub from '../stubs';
const localVue = createLocalVue();
localVue.use(VueApollo);
+const projectPath = 'path/to/project';
+const defaultBranchName = 'default';
+const maxAgents = MAX_LIST_COUNT;
+
describe('InstallAgentModal', () => {
let wrapper;
let apolloProvider;
@@ -45,10 +51,15 @@ describe('InstallAgentModal', () => {
const createWrapper = () => {
const provide = {
- projectPath: 'path/to/project',
+ projectPath,
kasAddress: 'kas.example.com',
};
+ const propsData = {
+ defaultBranchName,
+ maxAgents,
+ };
+
wrapper = shallowMount(InstallAgentModal, {
attachTo: document.body,
stubs: {
@@ -57,11 +68,26 @@ describe('InstallAgentModal', () => {
localVue,
apolloProvider,
provide,
+ propsData,
+ });
+ };
+
+ const writeQuery = () => {
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: getAgentsQuery,
+ variables: {
+ projectPath,
+ defaultBranchName,
+ first: MAX_LIST_COUNT,
+ last: null,
+ },
+ data: getAgentResponse.data,
});
};
const mockSelectedAgentResponse = () => {
createWrapper();
+ writeQuery();
wrapper.vm.setAgentName('agent-name');
findActionButton().vm.$emit('click');
@@ -95,7 +121,7 @@ describe('InstallAgentModal', () => {
it('renders a disabled next button', () => {
expect(findActionButton().isVisible()).toBe(true);
- expect(findActionButton().text()).toBe(i18n.next);
+ expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true);
});
});
@@ -126,7 +152,7 @@ describe('InstallAgentModal', () => {
it('creates an agent and token', () => {
expect(createAgentHandler).toHaveBeenCalledWith({
- input: { name: 'agent-name', projectPath: 'path/to/project' },
+ input: { name: 'agent-name', projectPath },
});
expect(createAgentTokenHandler).toHaveBeenCalledWith({
@@ -134,9 +160,9 @@ describe('InstallAgentModal', () => {
});
});
- it('renders a done button', () => {
+ it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
- expect(findActionButton().text()).toBe(i18n.done);
+ expect(findActionButton().text()).toBe(i18n.close);
expectDisabledAttribute(findActionButton(), false);
});
diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js
index 27b71a0d4b5..1a7ef84a6d9 100644
--- a/spec/frontend/clusters_list/mocks/apollo.js
+++ b/spec/frontend/clusters_list/mocks/apollo.js
@@ -1,8 +1,29 @@
+const agent = {
+ id: 'agent-id',
+ name: 'agent-name',
+ webPath: 'agent-webPath',
+};
+const token = {
+ id: 'token-id',
+ lastUsedAt: null,
+};
+const tokens = {
+ nodes: [token],
+};
+const pageInfo = {
+ endCursor: '',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+};
+const count = 1;
+
export const createAgentResponse = {
data: {
createClusterAgent: {
clusterAgent: {
- id: 'agent-id',
+ ...agent,
+ tokens,
},
errors: [],
},
@@ -13,7 +34,8 @@ export const createAgentErrorResponse = {
data: {
createClusterAgent: {
clusterAgent: {
- id: 'agent-id',
+ ...agent,
+ tokens,
},
errors: ['could not create agent'],
},
@@ -23,9 +45,7 @@ export const createAgentErrorResponse = {
export const createAgentTokenResponse = {
data: {
clusterAgentTokenCreate: {
- token: {
- id: 'token-id',
- },
+ token,
secret: 'mock-agent-token',
errors: [],
},
@@ -35,11 +55,22 @@ export const createAgentTokenResponse = {
export const createAgentTokenErrorResponse = {
data: {
clusterAgentTokenCreate: {
- token: {
- id: 'token-id',
- },
+ token,
secret: 'mock-agent-token',
errors: ['could not create agent token'],
},
},
};
+
+export const getAgentResponse = {
+ data: {
+ project: {
+ clusterAgents: { nodes: [{ ...agent, tokens }], pageInfo, count },
+ repository: {
+ tree: {
+ trees: { nodes: [{ ...agent, path: null }], pageInfo },
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/clusters_list/store/mutations_spec.js b/spec/frontend/clusters_list/store/mutations_spec.js
index c0fe634a703..ae264eee449 100644
--- a/spec/frontend/clusters_list/store/mutations_spec.js
+++ b/spec/frontend/clusters_list/store/mutations_spec.js
@@ -26,7 +26,7 @@ describe('Admin statistics panel mutations', () => {
expect(state.clusters).toBe(apiData.clusters);
expect(state.clustersPerPage).toBe(paginationInformation.perPage);
expect(state.hasAncestorClusters).toBe(apiData.has_ancestor_clusters);
- expect(state.totalCulsters).toBe(paginationInformation.total);
+ expect(state.totalClusters).toBe(paginationInformation.total);
});
});
@@ -57,4 +57,12 @@ describe('Admin statistics panel mutations', () => {
expect(state.page).toBe(123);
});
});
+
+ describe(`${types.SET_CLUSTERS_PER_PAGE}`, () => {
+ it('changes clustersPerPage value', () => {
+ mutations[types.SET_CLUSTERS_PER_PAGE](state, 123);
+
+ expect(state.clustersPerPage).toBe(123);
+ });
+ });
});
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 17f7be9d1d7..c376b58cc72 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlLoadingIcon, GlModal, GlTable } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import fixture from 'test_fixtures/pipelines/pipelines.json';
@@ -6,8 +6,13 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
+import { TOAST_MESSAGE } from '~/pipelines/constants';
import axios from '~/lib/utils/axios_utils';
+const $toast = {
+ show: jest.fn(),
+};
+
describe('Pipelines table in Commits and Merge requests', () => {
let wrapper;
let pipeline;
@@ -17,7 +22,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile');
const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findTable = () => wrapper.findComponent(GlTable);
+ const findTable = () => wrapper.findComponent(GlTableLite);
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findModal = () => wrapper.findComponent(GlModal);
@@ -30,6 +35,9 @@ describe('Pipelines table in Commits and Merge requests', () => {
errorStateSvgPath: 'foo',
...props,
},
+ mocks: {
+ $toast,
+ },
}),
);
};
@@ -178,6 +186,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
await waitForPromises();
});
+ it('displays a toast message during pipeline creation', async () => {
+ await findRunPipelineBtn().trigger('click');
+
+ expect($toast.show).toHaveBeenCalledWith(TOAST_MESSAGE);
+ });
+
it('on desktop, shows a loading button', async () => {
await findRunPipelineBtn().trigger('click');
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index 8a12ff3a01f..5e5345cbd2b 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -72,7 +72,7 @@ describe('ConfirmModal', () => {
it('starts with only JsHooks', () => {
expect(findJsHooks()).toHaveLength(buttons.length);
- expect(findModal()).not.toExist();
+ expect(findModal()).toBe(null);
});
describe('when button clicked', () => {
@@ -87,7 +87,7 @@ describe('ConfirmModal', () => {
describe('GlModal', () => {
it('is rendered', () => {
- expect(findModal()).toExist();
+ expect(findModal()).not.toBe(null);
expect(modalIsHidden()).toBe(false);
});
diff --git a/spec/frontend/content_editor/components/content_editor_error_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js
index 8723fb5a338..2ddcd8f024e 100644
--- a/spec/frontend/content_editor/components/content_editor_error_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -1,11 +1,11 @@
import { GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
+import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import { createTestEditor, emitEditorEvent } from '../test_utils';
-describe('content_editor/components/content_editor_error', () => {
+describe('content_editor/components/content_editor_alert', () => {
let wrapper;
let tiptapEditor;
@@ -14,7 +14,7 @@ describe('content_editor/components/content_editor_error', () => {
const createWrapper = async () => {
tiptapEditor = createTestEditor();
- wrapper = shallowMountExtended(ContentEditorError, {
+ wrapper = shallowMountExtended(ContentEditorAlert, {
provide: {
tiptapEditor,
},
@@ -28,22 +28,28 @@ describe('content_editor/components/content_editor_error', () => {
wrapper.destroy();
});
- it('renders error when content editor emits an error event', async () => {
- const error = 'error message';
+ it.each`
+ variant | message
+ ${'danger'} | ${'An error occurred'}
+ ${'warning'} | ${'A warning'}
+ `(
+ 'renders error when content editor emits an error event for variant: $variant',
+ async ({ message, variant }) => {
+ createWrapper();
- createWrapper();
-
- await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } });
+ await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message, variant } });
- expect(findErrorAlert().text()).toBe(error);
- });
+ expect(findErrorAlert().text()).toBe(message);
+ expect(findErrorAlert().attributes().variant).toBe(variant);
+ },
+ );
it('allows dismissing the error', async () => {
- const error = 'error message';
+ const message = 'error message';
createWrapper();
- await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } });
+ await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message } });
findErrorAlert().vm.$emit('dismiss');
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 3d1ef03083d..9a772c41e52 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -3,7 +3,7 @@ import { EditorContent } from '@tiptap/vue-2';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
-import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
+import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
@@ -111,10 +111,10 @@ describe('ContentEditor', () => {
]);
});
- it('renders content_editor_error component', () => {
+ it('renders content_editor_alert component', () => {
createWrapper();
- expect(wrapper.findComponent(ContentEditorError).exists()).toBe(true);
+ expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true);
});
describe('when loading content', () => {
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index e48f59f6d9c..6017a145a87 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -11,13 +11,13 @@ jest.mock('prosemirror-tables');
describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
let editor;
- let getPos;
+ let node;
const createWrapper = async (propsData = { cellType: 'td' }) => {
wrapper = shallowMountExtended(TableCellBaseWrapper, {
propsData: {
editor,
- getPos,
+ node,
...propsData,
},
});
@@ -36,7 +36,7 @@ describe('content/components/wrappers/table_cell_base', () => {
const setCurrentPositionInCell = () => {
const { $cursor } = editor.state.selection;
- getPos.mockReturnValue($cursor.pos - $cursor.parentOffset - 1);
+ jest.spyOn($cursor, 'node').mockReturnValue(node);
};
const mockDropdownHide = () => {
/*
@@ -48,7 +48,7 @@ describe('content/components/wrappers/table_cell_base', () => {
};
beforeEach(() => {
- getPos = jest.fn();
+ node = {};
editor = createTestEditor({});
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
index 5d26c44ba03..2aefbc77545 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
@@ -6,19 +6,19 @@ import { createTestEditor } from '../../test_utils';
describe('content/components/wrappers/table_cell_body', () => {
let wrapper;
let editor;
- let getPos;
+ let node;
const createWrapper = async () => {
wrapper = shallowMount(TableCellBodyWrapper, {
propsData: {
editor,
- getPos,
+ node,
},
});
};
beforeEach(() => {
- getPos = jest.fn();
+ node = {};
editor = createTestEditor({});
});
@@ -30,7 +30,7 @@ describe('content/components/wrappers/table_cell_body', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
editor,
- getPos,
+ node,
cellType: 'td',
});
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
index e561191418d..e48df8734a6 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
@@ -6,19 +6,19 @@ import { createTestEditor } from '../../test_utils';
describe('content/components/wrappers/table_cell_header', () => {
let wrapper;
let editor;
- let getPos;
+ let node;
const createWrapper = async () => {
wrapper = shallowMount(TableCellHeaderWrapper, {
propsData: {
editor,
- getPos,
+ node,
},
});
};
beforeEach(() => {
- getPos = jest.fn();
+ node = {};
editor = createTestEditor({});
});
@@ -30,7 +30,7 @@ describe('content/components/wrappers/table_cell_header', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
editor,
- getPos,
+ node,
cellType: 'th',
});
});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index d4f05a25bd6..d2d2cd98a78 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -74,10 +74,10 @@ describe('content_editor/extensions/attachment', () => {
});
it.each`
- eventType | propName | eventData | output
- ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [attachmentFile] } }} | ${true}
- ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [] } }} | ${undefined}
- ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { files: [attachmentFile] } }} | ${true}
+ eventType | propName | eventData | output
+ ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
+ ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [] } }} | ${undefined}
+ ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
`('handles $eventType properly', ({ eventType, propName, eventData, output }) => {
const event = Object.assign(new Event(eventType), eventData);
const handled = tiptapEditor.view.someProp(propName, (eventHandler) => {
@@ -157,11 +157,11 @@ describe('content_editor/extensions/attachment', () => {
});
});
- it('emits an error event that includes an error message', (done) => {
+ it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: imageFile });
- tiptapEditor.on('error', ({ error }) => {
- expect(error).toBe('An error occurred while uploading the image. Please try again.');
+ tiptapEditor.on('alert', ({ message }) => {
+ expect(message).toBe('An error occurred while uploading the image. Please try again.');
done();
});
});
@@ -233,11 +233,11 @@ describe('content_editor/extensions/attachment', () => {
});
});
- it('emits an error event that includes an error message', (done) => {
+ it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
- tiptapEditor.on('error', ({ error }) => {
- expect(error).toBe('An error occurred while uploading the file. Please try again.');
+ tiptapEditor.on('alert', ({ message }) => {
+ expect(message).toBe('An error occurred while uploading the file. Please try again.');
done();
});
});
diff --git a/spec/frontend/content_editor/extensions/blockquote_spec.js b/spec/frontend/content_editor/extensions/blockquote_spec.js
index c5b5044352d..1644647ba69 100644
--- a/spec/frontend/content_editor/extensions/blockquote_spec.js
+++ b/spec/frontend/content_editor/extensions/blockquote_spec.js
@@ -1,19 +1,37 @@
-import { multilineInputRegex } from '~/content_editor/extensions/blockquote';
+import Blockquote from '~/content_editor/extensions/blockquote';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/blockquote', () => {
- describe.each`
- input | matches
- ${'>>> '} | ${true}
- ${' >>> '} | ${true}
- ${'\t>>> '} | ${true}
- ${'>> '} | ${false}
- ${'>>>x '} | ${false}
- ${'> '} | ${false}
- `('multilineInputRegex', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
- const match = new RegExp(multilineInputRegex).test(input);
+ let tiptapEditor;
+ let doc;
+ let p;
+ let blockquote;
- expect(match).toBe(matches);
- });
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Blockquote] });
+
+ ({
+ builders: { doc, p, blockquote },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ blockquote: { nodeType: Blockquote.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'>>> '} | ${() => blockquote({ multiline: true }, p())}
+ ${'> '} | ${() => blockquote(p())}
+ ${' >>> '} | ${() => blockquote({ multiline: true }, p())}
+ ${'>> '} | ${() => p()}
+ ${'>>>x '} | ${() => p()}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/emoji_spec.js b/spec/frontend/content_editor/extensions/emoji_spec.js
index c1b8dc9bdbb..939c46e991a 100644
--- a/spec/frontend/content_editor/extensions/emoji_spec.js
+++ b/spec/frontend/content_editor/extensions/emoji_spec.js
@@ -1,6 +1,6 @@
import { initEmojiMock } from 'helpers/emoji';
import Emoji from '~/content_editor/extensions/emoji';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/emoji', () => {
let tiptapEditor;
@@ -28,18 +28,16 @@ describe('content_editor/extensions/emoji', () => {
describe('when typing a valid emoji input rule', () => {
it('inserts an emoji node', () => {
- const { view } = tiptapEditor;
- const { selection } = view.state;
const expectedDoc = doc(
p(
' ',
emoji({ moji: '❤', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }),
),
);
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, ':heart:'));
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: ':heart:' });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/frontmatter_spec.js b/spec/frontend/content_editor/extensions/frontmatter_spec.js
new file mode 100644
index 00000000000..517f6947b9a
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/frontmatter_spec.js
@@ -0,0 +1,30 @@
+import Frontmatter from '~/content_editor/extensions/frontmatter';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/frontmatter', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Frontmatter] });
+
+ ({
+ builders: { doc, p },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ frontmatter: { nodeType: Frontmatter.name },
+ },
+ }));
+ });
+
+ it('does not insert a frontmatter block when executing code block input rule', () => {
+ const expectedDoc = doc(p(''));
+ const inputRuleText = '``` ';
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
index a1bc7f0e8ed..322c04a42e1 100644
--- a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
+++ b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
@@ -1,20 +1,39 @@
-import { hrInputRuleRegExp } from '~/content_editor/extensions/horizontal_rule';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/horizontal_rule', () => {
- describe.each`
- input | matches
- ${'---'} | ${true}
- ${'--'} | ${false}
- ${'---x'} | ${false}
- ${' ---x'} | ${false}
- ${' --- '} | ${false}
- ${'x---x'} | ${false}
- ${'x---'} | ${false}
- `('hrInputRuleRegExp', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
- const match = new RegExp(hrInputRuleRegExp).test(input);
+ let tiptapEditor;
+ let doc;
+ let p;
+ let horizontalRule;
- expect(match).toBe(matches);
- });
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [HorizontalRule] });
+
+ ({
+ builders: { doc, p, horizontalRule },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ horizontalRule: { nodeType: HorizontalRule.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNodes
+ ${'---'} | ${() => [p(), horizontalRule()]}
+ ${'--'} | ${() => [p()]}
+ ${'---x'} | ${() => [p()]}
+ ${' ---x'} | ${() => [p()]}
+ ${' --- '} | ${() => [p()]}
+ ${'x---x'} | ${() => [p()]}
+ ${'x---'} | ${() => [p()]}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNodes }) => {
+ const expectedDoc = doc(...insertedNodes());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/inline_diff_spec.js b/spec/frontend/content_editor/extensions/inline_diff_spec.js
index 63cdf665e7f..99c559a20b1 100644
--- a/spec/frontend/content_editor/extensions/inline_diff_spec.js
+++ b/spec/frontend/content_editor/extensions/inline_diff_spec.js
@@ -1,27 +1,43 @@
-import { inputRegexAddition, inputRegexDeletion } from '~/content_editor/extensions/inline_diff';
+import InlineDiff from '~/content_editor/extensions/inline_diff';
+import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/inline_diff', () => {
- describe.each`
- inputRegex | description | input | matches
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+world+}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+ world +}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello {+ world+}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello world +}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello with \nnewline+}'} | ${false}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+open only'} | ${false}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'close only+}'} | ${false}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{-world-}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{- world -}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello {- world-}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-hello world -}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{+hello with \nnewline+}'} | ${false}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-open only'} | ${false}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'close only-}'} | ${false}
- `('$description', ({ inputRegex, input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
- const match = new RegExp(inputRegex).test(input);
+ let tiptapEditor;
+ let doc;
+ let p;
+ let inlineDiff;
- expect(match).toBe(matches);
- });
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [InlineDiff] });
+ ({
+ builders: { doc, p, inlineDiff },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ inlineDiff: { markType: InlineDiff.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'hello{+world+}'} | ${() => p('hello', inlineDiff('world'))}
+ ${'hello{+ world +}'} | ${() => p('hello', inlineDiff(' world '))}
+ ${'{+hello with \nnewline+}'} | ${() => p('{+hello with newline+}')}
+ ${'{+open only'} | ${() => p('{+open only')}
+ ${'close only+}'} | ${() => p('close only+}')}
+ ${'hello{-world-}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, 'world'))}
+ ${'hello{- world -}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, ' world '))}
+ ${'hello {- world-}'} | ${() => p('hello ', inlineDiff({ type: 'deletion' }, ' world'))}
+ ${'{-hello world -}'} | ${() => p(inlineDiff({ type: 'deletion' }, 'hello world '))}
+ ${'{-hello with \nnewline-}'} | ${() => p('{-hello with newline-}')}
+ ${'{-open only'} | ${() => p('{-open only')}
+ ${'close only-}'} | ${() => p('close only-}')}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
index 026b2a06df3..ead898554d1 100644
--- a/spec/frontend/content_editor/extensions/link_spec.js
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -1,61 +1,46 @@
-import {
- markdownLinkSyntaxInputRuleRegExp,
- urlSyntaxRegExp,
- extractHrefFromMarkdownLink,
-} from '~/content_editor/extensions/link';
+import Link from '~/content_editor/extensions/link';
+import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/link', () => {
- describe.each`
- input | matches
- ${'[gitlab](https://gitlab.com)'} | ${true}
- ${'[documentation](readme.md)'} | ${true}
- ${'[link 123](readme.md)'} | ${true}
- ${'[link 123](read me.md)'} | ${true}
- ${'text'} | ${false}
- ${'documentation](readme.md'} | ${false}
- ${'https://www.google.com'} | ${false}
- `('markdownLinkSyntaxInputRuleRegExp', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
- const match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
-
- expect(Boolean(match?.groups.href)).toBe(matches);
- });
+ let tiptapEditor;
+ let doc;
+ let p;
+ let link;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Link] });
+ ({
+ builders: { doc, p, link },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ link: { markType: Link.name },
+ },
+ }));
});
- describe.each`
- input | matches
- ${'http://example.com '} | ${true}
- ${'https://example.com '} | ${true}
- ${'www.example.com '} | ${true}
- ${'example.com/ab.html '} | ${false}
- ${'text'} | ${false}
- ${' http://example.com '} | ${true}
- ${'https://www.google.com '} | ${true}
- `('urlSyntaxRegExp', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
- const match = new RegExp(urlSyntaxRegExp).exec(input);
-
- expect(Boolean(match?.groups.href)).toBe(matches);
- });
+ afterEach(() => {
+ tiptapEditor.destroy();
});
- describe('extractHrefFromMarkdownLink', () => {
- const input = '[gitlab](https://gitlab.com)';
- const href = 'https://gitlab.com';
- let match;
- let result;
-
- beforeEach(() => {
- match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
- result = extractHrefFromMarkdownLink(match);
- });
-
- it('extracts the url from a markdown link captured by markdownLinkSyntaxInputRuleRegExp', () => {
- expect(result).toEqual({ href });
- });
-
- it('makes sure that url text is the last capture group', () => {
- expect(match[match.length - 1]).toEqual('gitlab');
- });
+ it.each`
+ input | insertedNode
+ ${'[gitlab](https://gitlab.com)'} | ${() => p(link({ href: 'https://gitlab.com' }, 'gitlab'))}
+ ${'[documentation](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'documentation'))}
+ ${'[link 123](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'link 123'))}
+ ${'[link 123](read me.md)'} | ${() => p(link({ href: 'read me.md' }, 'link 123'))}
+ ${'text'} | ${() => p('text')}
+ ${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
+ ${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
+ ${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
+ ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
+ ${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
+ ${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/math_inline_spec.js b/spec/frontend/content_editor/extensions/math_inline_spec.js
index 82eb85477de..abf10317b5a 100644
--- a/spec/frontend/content_editor/extensions/math_inline_spec.js
+++ b/spec/frontend/content_editor/extensions/math_inline_spec.js
@@ -1,5 +1,5 @@
import MathInline from '~/content_editor/extensions/math_inline';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/math_inline', () => {
let tiptapEditor;
@@ -26,16 +26,9 @@ describe('content_editor/extensions/math_inline', () => {
${'$`a^2`'} | ${() => p('$`a^2`')}
${'`a^2`$'} | ${() => p('`a^2`$')}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
- const { view } = tiptapEditor;
const expectedDoc = doc(insertedNode());
- tiptapEditor.chain().setContent(input).setTextSelection(0).run();
-
- const { state } = tiptapEditor;
- const { selection } = state;
-
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, input.length + 1, input));
+ triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
diff --git a/spec/frontend/content_editor/extensions/table_of_contents_spec.js b/spec/frontend/content_editor/extensions/table_of_contents_spec.js
index 83818899c17..0ddd88b39fe 100644
--- a/spec/frontend/content_editor/extensions/table_of_contents_spec.js
+++ b/spec/frontend/content_editor/extensions/table_of_contents_spec.js
@@ -1,13 +1,17 @@
import TableOfContents from '~/content_editor/extensions/table_of_contents';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
-describe('content_editor/extensions/emoji', () => {
+describe('content_editor/extensions/table_of_contents', () => {
let tiptapEditor;
- let builders;
+ let doc;
+ let tableOfContents;
+ let p;
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [TableOfContents] });
- ({ builders } = createDocBuilder({
+ ({
+ builders: { doc, p, tableOfContents },
+ } = createDocBuilder({
tiptapEditor,
names: { tableOfContents: { nodeType: TableOfContents.name } },
}));
@@ -15,20 +19,16 @@ describe('content_editor/extensions/emoji', () => {
it.each`
input | insertedNode
- ${'[[_TOC_]]'} | ${'tableOfContents'}
- ${'[TOC]'} | ${'tableOfContents'}
- ${'[toc]'} | ${'p'}
- ${'TOC'} | ${'p'}
- ${'[_TOC_]'} | ${'p'}
- ${'[[TOC]]'} | ${'p'}
+ ${'[[_TOC_]]'} | ${() => tableOfContents()}
+ ${'[TOC]'} | ${() => tableOfContents()}
+ ${'[toc]'} | ${() => p()}
+ ${'TOC'} | ${() => p()}
+ ${'[_TOC_]'} | ${() => p()}
+ ${'[[TOC]]'} | ${() => p()}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
- const { doc } = builders;
- const { view } = tiptapEditor;
- const { selection } = view.state;
- const expectedDoc = doc(builders[insertedNode]());
+ const expectedDoc = doc(insertedNode());
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input));
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
diff --git a/spec/frontend/content_editor/extensions/table_spec.js b/spec/frontend/content_editor/extensions/table_spec.js
new file mode 100644
index 00000000000..121fe9192db
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/table_spec.js
@@ -0,0 +1,102 @@
+import Bold from '~/content_editor/extensions/bold';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import ListItem from '~/content_editor/extensions/list_item';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableRow from '~/content_editor/extensions/table_row';
+import TableHeader from '~/content_editor/extensions/table_header';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/table', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let table;
+ let tableHeader;
+ let tableCell;
+ let tableRow;
+ let initialDoc;
+ let mockAlert;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({
+ extensions: [Table, TableCell, TableRow, TableHeader, BulletList, Bold, ListItem],
+ });
+
+ ({
+ builders: { doc, p, table, tableCell, tableHeader, tableRow },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ bold: { markType: Bold.name },
+ table: { nodeType: Table.name },
+ tableHeader: { nodeType: TableHeader.name },
+ tableCell: { nodeType: TableCell.name },
+ tableRow: { nodeType: TableRow.name },
+ bulletList: { nodeType: BulletList.name },
+ listItem: { nodeType: ListItem.name },
+ },
+ }));
+
+ initialDoc = doc(
+ table(
+ { isMarkdown: true },
+ tableRow(tableHeader(p('This is')), tableHeader(p('a table'))),
+ tableRow(tableCell(p('this is')), tableCell(p('the first row'))),
+ ),
+ );
+
+ mockAlert = jest.fn();
+ });
+
+ it('triggers a warning (just once) if the table is markdown, but the changes in the document will render an HTML table instead', () => {
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.on('alert', mockAlert);
+
+ tiptapEditor.commands.setTextSelection({ from: 20, to: 22 });
+ tiptapEditor.commands.toggleBulletList();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).toHaveBeenCalled();
+
+ mockAlert.mockReset();
+
+ tiptapEditor.commands.setTextSelection({ from: 4, to: 6 });
+ tiptapEditor.commands.toggleBulletList();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger a warning if the table is markdown, and the changes in the document can generate a markdown table', () => {
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.on('alert', mockAlert);
+
+ tiptapEditor.commands.setTextSelection({ from: 20, to: 22 });
+ tiptapEditor.commands.toggleBold();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger any warnings if the table is not markdown', () => {
+ initialDoc = doc(
+ table(
+ tableRow(tableHeader(p('This is')), tableHeader(p('a table'))),
+ tableRow(tableCell(p('this is')), tableCell(p('the first row'))),
+ ),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.on('alert', mockAlert);
+
+ tiptapEditor.commands.setTextSelection({ from: 20, to: 22 });
+ tiptapEditor.commands.toggleBulletList();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/word_break_spec.js b/spec/frontend/content_editor/extensions/word_break_spec.js
new file mode 100644
index 00000000000..23167269d7d
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/word_break_spec.js
@@ -0,0 +1,35 @@
+import WordBreak from '~/content_editor/extensions/word_break';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/word_break', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let wordBreak;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [WordBreak] });
+
+ ({
+ builders: { doc, p, wordBreak },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ wordBreak: { nodeType: WordBreak.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'<wbr>'} | ${() => p(wordBreak())}
+ ${'<wbr'} | ${() => p()}
+ ${'wbr>'} | ${() => p()}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 33056ab9e4a..cfd93c2df10 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -34,10 +34,6 @@ import { createTestEditor, createDocBuilder } from '../test_utils';
jest.mock('~/emoji');
-jest.mock('~/content_editor/services/feature_flags', () => ({
- isBlockTablesFeatureEnabled: jest.fn().mockReturnValue(true),
-}));
-
const tiptapEditor = createTestEditor({
extensions: [
Blockquote,
diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
index afe09a75f16..459780cc7cf 100644
--- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
+++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
@@ -10,7 +10,7 @@ import Heading from '~/content_editor/extensions/heading';
import ListItem from '~/content_editor/extensions/list_item';
import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
-import { createTestEditor } from '../test_utils';
+import { createTestEditor, triggerNodeInputRule } from '../test_utils';
describe('content_editor/services/track_input_rules_and_shortcuts', () => {
let trackingSpy;
@@ -70,14 +70,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('when creating a heading using an input rule', () => {
it('sends a tracking event indicating that a heading was created using an input rule', async () => {
const nodeName = Heading.name;
- const { view } = editor;
- const { selection } = view.state;
-
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, '## '));
-
- editor.chain().insertContent(HEADING_TEXT).run();
-
+ triggerNodeInputRule({ tiptapEditor: editor, inputRuleText: '## ' });
expect(trackingSpy).toHaveBeenCalledWith(undefined, INPUT_RULE_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL,
property: `${nodeName}`,
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index cf5aa3f2938..b236c630e13 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -119,3 +119,26 @@ export const createTestContentEditorExtension = ({ commands = [] } = {}) => {
},
};
};
+
+export const triggerNodeInputRule = ({ tiptapEditor, inputRuleText }) => {
+ const { view } = tiptapEditor;
+ const { state } = tiptapEditor;
+ const { selection } = state;
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, inputRuleText));
+};
+
+export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => {
+ const { view } = tiptapEditor;
+
+ tiptapEditor.chain().setContent(inputRuleText).setTextSelection(0).run();
+
+ const { state } = tiptapEditor;
+ const { selection } = state;
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) =>
+ f(view, selection.from, inputRuleText.length + 1, inputRuleText),
+ );
+};
diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js
index 8878891701f..9f07eea433a 100644
--- a/spec/frontend/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/create_merge_request_dropdown_spec.js
@@ -46,7 +46,10 @@ describe('CreateMergeRequestDropdown', () => {
dropdown
.getRef('contains#hash')
.then(() => {
- expect(axios.get).toHaveBeenCalledWith(endpoint);
+ expect(axios.get).toHaveBeenCalledWith(
+ endpoint,
+ expect.objectContaining({ cancelToken: expect.anything() }),
+ );
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
new file mode 100644
index 00000000000..79b85969eb4
--- /dev/null
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -0,0 +1,60 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import ContactsRoot from '~/crm/components/contacts_root.vue';
+import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
+import { getGroupContactsQueryResponse } from './mock_data';
+
+jest.mock('~/flash');
+
+describe('Customer relations contacts root app', () => {
+ Vue.use(VueApollo);
+ let wrapper;
+ let fakeApollo;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
+ const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
+
+ const mountComponent = ({
+ queryHandler = successQueryHandler,
+ mountFunction = shallowMountExtended,
+ } = {}) => {
+ fakeApollo = createMockApollo([[getGroupContactsQuery, queryHandler]]);
+ wrapper = mountFunction(ContactsRoot, {
+ provide: { groupFullPath: 'flightjs' },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('should render loading spinner', () => {
+ mountComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('should render error message on reject', async () => {
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ it('renders correct results', async () => {
+ mountComponent({ mountFunction: mountExtended });
+ await waitForPromises();
+
+ expect(findRowByName(/Marty/i)).toHaveLength(1);
+ expect(findRowByName(/George/i)).toHaveLength(1);
+ expect(findRowByName(/jd@gitlab.com/i)).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
new file mode 100644
index 00000000000..4197621aaa6
--- /dev/null
+++ b/spec/frontend/crm/mock_data.js
@@ -0,0 +1,81 @@
+export const getGroupContactsQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/26',
+ contacts: {
+ nodes: [
+ {
+ __typename: 'CustomerRelationsContact',
+ id: 'gid://gitlab/CustomerRelations::Contact/12',
+ firstName: 'Marty',
+ lastName: 'McFly',
+ email: 'example@gitlab.com',
+ phone: null,
+ description: null,
+ organization: {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/2',
+ name: 'Tech Giant Inc',
+ },
+ },
+ {
+ __typename: 'CustomerRelationsContact',
+ id: 'gid://gitlab/CustomerRelations::Contact/16',
+ firstName: 'Boy',
+ lastName: 'George',
+ email: null,
+ phone: null,
+ description: null,
+ organization: null,
+ },
+ {
+ __typename: 'CustomerRelationsContact',
+ id: 'gid://gitlab/CustomerRelations::Contact/13',
+ firstName: 'Jane',
+ lastName: 'Doe',
+ email: 'jd@gitlab.com',
+ phone: '+44 44 4444 4444',
+ description: 'Vice President',
+ organization: null,
+ },
+ ],
+ __typename: 'CustomerRelationsContactConnection',
+ },
+ },
+ },
+};
+
+export const getGroupOrganizationsQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/26',
+ organizations: {
+ nodes: [
+ {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/1',
+ name: 'Test Inc',
+ defaultRate: 100,
+ description: null,
+ },
+ {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/2',
+ name: 'ABC Company',
+ defaultRate: 110,
+ description: 'VIP',
+ },
+ {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/3',
+ name: 'GitLab',
+ defaultRate: 120,
+ description: null,
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
new file mode 100644
index 00000000000..a69a099e03d
--- /dev/null
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -0,0 +1,60 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import OrganizationsRoot from '~/crm/components/organizations_root.vue';
+import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
+import { getGroupOrganizationsQueryResponse } from './mock_data';
+
+jest.mock('~/flash');
+
+describe('Customer relations organizations root app', () => {
+ Vue.use(VueApollo);
+ let wrapper;
+ let fakeApollo;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
+ const successQueryHandler = jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse);
+
+ const mountComponent = ({
+ queryHandler = successQueryHandler,
+ mountFunction = shallowMountExtended,
+ } = {}) => {
+ fakeApollo = createMockApollo([[getGroupOrganizationsQuery, queryHandler]]);
+ wrapper = mountFunction(OrganizationsRoot, {
+ provide: { groupFullPath: 'flightjs' },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('should render loading spinner', () => {
+ mountComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('should render error message on reject', async () => {
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ it('renders correct results', async () => {
+ mountComponent({ mountFunction: mountExtended });
+ await waitForPromises();
+
+ expect(findRowByName(/Test Inc/i)).toHaveLength(1);
+ expect(findRowByName(/VIP/i)).toHaveLength(1);
+ expect(findRowByName(/120/i)).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
index c41adf523f8..2001f5c1441 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
@@ -4,8 +4,6 @@ import { TEST_HOST } from 'helpers/test_constants';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import axios from '~/lib/utils/axios_utils';
-const { CancelToken } = axios;
-
describe('custom metrics form fields component', () => {
let wrapper;
let mockAxios;
@@ -116,14 +114,14 @@ describe('custom metrics form fields component', () => {
it('receives and validates a persisted value', () => {
const query = 'persistedQuery';
- const axiosPost = jest.spyOn(axios, 'post');
- const source = CancelToken.source();
+ jest.spyOn(axios, 'post');
+
mountComponent({ metricPersisted: true, ...makeFormData({ query }) });
- expect(axiosPost).toHaveBeenCalledWith(
+ expect(axios.post).toHaveBeenCalledWith(
validateQueryPath,
{ query },
- { cancelToken: source.token },
+ expect.objectContaining({ cancelToken: expect.anything() }),
);
expect(getNamedInput(queryInputName).value).toBe(query);
jest.runAllTimers();
diff --git a/spec/frontend/cycle_analytics/metric_popover_spec.js b/spec/frontend/cycle_analytics/metric_popover_spec.js
new file mode 100644
index 00000000000..5a622fcacd5
--- /dev/null
+++ b/spec/frontend/cycle_analytics/metric_popover_spec.js
@@ -0,0 +1,102 @@
+import { GlLink, GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MetricPopover from '~/cycle_analytics/components/metric_popover.vue';
+
+const MOCK_METRIC = {
+ key: 'deployment-frequency',
+ label: 'Deployment Frequency',
+ value: '10.0',
+ unit: 'per day',
+ description: 'Average number of deployments to production per day.',
+ links: [],
+};
+
+describe('MetricPopover', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ return shallowMountExtended(MetricPopover, {
+ propsData: {
+ target: 'deployment-frequency',
+ ...props,
+ },
+ stubs: {
+ 'gl-popover': { template: '<div><slot name="title"></slot><slot></slot></div>' },
+ },
+ });
+ };
+
+ const findMetricLabel = () => wrapper.findByTestId('metric-label');
+ const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]');
+ const findMetricDescription = () => wrapper.findByTestId('metric-description');
+ const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link');
+ const findMetricDocsLinkIcon = () => findMetricDocsLink().find(GlIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the metric label', () => {
+ wrapper = createComponent({ metric: MOCK_METRIC });
+ expect(findMetricLabel().text()).toBe(MOCK_METRIC.label);
+ });
+
+ it('renders the metric description', () => {
+ wrapper = createComponent({ metric: MOCK_METRIC });
+ expect(findMetricDescription().text()).toBe(MOCK_METRIC.description);
+ });
+
+ describe('with links', () => {
+ const links = [
+ {
+ name: 'Deployment frequency',
+ url: '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency',
+ label: 'Dashboard',
+ },
+ {
+ name: 'Another link',
+ url: '/groups/gitlab-org/-/analytics/another-link',
+ label: 'Another link',
+ },
+ ];
+ const docsLink = {
+ name: 'Deployment frequency',
+ url: '/help/user/analytics/index#definitions',
+ label: 'Go to docs',
+ docs_link: true,
+ };
+ const linksWithDocs = [...links, docsLink];
+
+ describe.each`
+ hasDocsLink | allLinks | displayedMetricLinks
+ ${true} | ${linksWithDocs} | ${links}
+ ${false} | ${links} | ${links}
+ `(
+ 'when one link has docs_link=$hasDocsLink',
+ ({ hasDocsLink, allLinks, displayedMetricLinks }) => {
+ beforeEach(() => {
+ wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
+ });
+
+ displayedMetricLinks.forEach((link, idx) => {
+ it(`renders a link for "${link.name}"`, () => {
+ const allLinkContainers = findAllMetricLinks();
+
+ expect(allLinkContainers.at(idx).text()).toContain(link.name);
+ expect(allLinkContainers.at(idx).find(GlLink).attributes('href')).toBe(link.url);
+ });
+ });
+
+ it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
+ expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
+
+ if (hasDocsLink) {
+ expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
+ expect(findMetricDocsLink().text()).toBe(docsLink.label);
+ expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
+ }
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index 1882457960a..c482bd4e910 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -1,10 +1,14 @@
-/* eslint-disable import/no-deprecated */
+import valueStreamAnalyticsStages from 'test_fixtures/projects/analytics/value_stream_analytics/stages.json';
+import issueStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/issue.json';
+import planStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/plan.json';
+import reviewStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/review.json';
+import codeStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/code.json';
+import testStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/test.json';
+import stagingStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/staging.json';
-import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import {
DEFAULT_VALUE_STREAM,
- DEFAULT_DAYS_IN_PAST,
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
@@ -12,6 +16,7 @@ import {
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
+const DEFAULT_DAYS_IN_PAST = 30;
export const createdBefore = new Date(2019, 0, 14);
export const createdAfter = getDateInPast(createdBefore, DEFAULT_DAYS_IN_PAST);
@@ -20,28 +25,16 @@ export const deepCamelCase = (obj) => convertObjectPropsToCamelCase(obj, { deep:
export const getStageByTitle = (stages, title) =>
stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {};
-const fixtureEndpoints = {
- customizableCycleAnalyticsStagesAndEvents:
- 'projects/analytics/value_stream_analytics/stages.json',
- stageEvents: (stage) => `projects/analytics/value_stream_analytics/events/${stage}.json`,
- metricsData: 'projects/analytics/value_stream_analytics/summary.json',
-};
-
-export const metricsData = getJSONFixture(fixtureEndpoints.metricsData);
-
-export const customizableStagesAndEvents = getJSONFixture(
- fixtureEndpoints.customizableCycleAnalyticsStagesAndEvents,
-);
-
export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging'];
-const stageFixtures = defaultStages.reduce((acc, stage) => {
- const events = getJSONFixture(fixtureEndpoints.stageEvents(stage));
- return {
- ...acc,
- [stage]: events,
- };
-}, {});
+const stageFixtures = {
+ issue: issueStageFixtures,
+ plan: planStageFixtures,
+ review: reviewStageFixtures,
+ code: codeStageFixtures,
+ test: testStageFixtures,
+ staging: stagingStageFixtures,
+};
export const summary = [
{ value: '20', title: 'New Issues' },
@@ -260,7 +253,7 @@ export const selectedProjects = [
},
];
-export const rawValueStreamStages = customizableStagesAndEvents.stages;
+export const rawValueStreamStages = valueStreamAnalyticsStages.stages;
export const valueStreamStages = rawValueStreamStages.map((s) =>
convertObjectPropsToCamelCase(s, { deep: true }),
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index 993e6b6b73a..e775e941b4c 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -57,22 +57,12 @@ describe('Project Value Stream Analytics actions', () => {
const mutationTypes = (arr) => arr.map(({ type }) => type);
- const mockFetchStageDataActions = [
- { type: 'setLoading', payload: true },
- { type: 'fetchCycleAnalyticsData' },
- { type: 'fetchStageData' },
- { type: 'fetchStageMedians' },
- { type: 'fetchStageCountValues' },
- { type: 'setLoading', payload: false },
- ];
-
describe.each`
- action | payload | expectedActions | expectedMutations
- ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
- ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
- ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
- ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
- ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
+ action | payload | expectedActions | expectedMutations
+ ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${[{ type: 'refetchStageData' }]} | ${[mockSetDateActionCommit]}
+ ${'setFilters'} | ${[]} | ${[{ type: 'refetchStageData' }]} | ${[]}
+ ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'refetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
+ ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () =>
@@ -86,9 +76,18 @@ describe('Project Value Stream Analytics actions', () => {
});
describe('initializeVsa', () => {
- let mockDispatch;
- let mockCommit;
- const payload = { endpoints: mockEndpoints };
+ const selectedAuthor = 'Author';
+ const selectedMilestone = 'Milestone 1';
+ const selectedAssigneeList = ['Assignee 1', 'Assignee 2'];
+ const selectedLabelList = ['Label 1', 'Label 2'];
+ const payload = {
+ endpoints: mockEndpoints,
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ selectedStage,
+ };
const mockFilterEndpoints = {
groupEndpoint: 'foo',
labelsEndpoint: mockLabelsPath,
@@ -96,27 +95,63 @@ describe('Project Value Stream Analytics actions', () => {
projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
};
+ it('will dispatch fetchValueStreams actions and commit SET_LOADING and INITIALIZE_VSA', () => {
+ return testAction({
+ action: actions.initializeVsa,
+ state: {},
+ payload,
+ expectedMutations: [
+ { type: 'INITIALIZE_VSA', payload },
+ { type: 'SET_LOADING', payload: true },
+ { type: 'SET_LOADING', payload: false },
+ ],
+ expectedActions: [
+ { type: 'filters/setEndpoints', payload: mockFilterEndpoints },
+ {
+ type: 'filters/initialize',
+ payload: { selectedAuthor, selectedMilestone, selectedAssigneeList, selectedLabelList },
+ },
+ { type: 'fetchValueStreams' },
+ { type: 'setInitialStage', payload: selectedStage },
+ ],
+ });
+ });
+ });
+
+ describe('setInitialStage', () => {
beforeEach(() => {
- mockDispatch = jest.fn(() => Promise.resolve());
- mockCommit = jest.fn();
+ state = { ...state, stages: allowedStages };
});
- it('will dispatch the setLoading and fetchValueStreams actions and commit INITIALIZE_VSA', async () => {
- await actions.initializeVsa(
- {
- ...state,
- dispatch: mockDispatch,
- commit: mockCommit,
- },
- payload,
- );
- expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints });
-
- expect(mockDispatch).toHaveBeenCalledTimes(4);
- expect(mockDispatch).toHaveBeenCalledWith('filters/setEndpoints', mockFilterEndpoints);
- expect(mockDispatch).toHaveBeenCalledWith('setLoading', true);
- expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams');
- expect(mockDispatch).toHaveBeenCalledWith('setLoading', false);
+ describe('with a selected stage', () => {
+ it('will commit `SET_SELECTED_STAGE` and fetchValueStreamStageData actions', () => {
+ const fakeStage = { ...selectedStage, id: 'fake', name: 'fake-stae' };
+ return testAction({
+ action: actions.setInitialStage,
+ state,
+ payload: fakeStage,
+ expectedMutations: [
+ {
+ type: 'SET_SELECTED_STAGE',
+ payload: fakeStage,
+ },
+ ],
+ expectedActions: [{ type: 'fetchValueStreamStageData' }],
+ });
+ });
+ });
+
+ describe('without a selected stage', () => {
+ it('will select the first stage from the value stream', () => {
+ const [firstStage] = allowedStages;
+ testAction({
+ action: actions.setInitialStage,
+ state,
+ payload: null,
+ expectedMutations: [{ type: 'SET_SELECTED_STAGE', payload: firstStage }],
+ expectedActions: [{ type: 'fetchValueStreamStageData' }],
+ });
+ });
});
});
@@ -270,12 +305,7 @@ describe('Project Value Stream Analytics actions', () => {
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
- expectedActions: [
- { type: 'receiveValueStreamsSuccess' },
- { type: 'setSelectedStage' },
- { type: 'fetchStageMedians' },
- { type: 'fetchStageCountValues' },
- ],
+ expectedActions: [{ type: 'receiveValueStreamsSuccess' }],
}));
describe('with a failing request', () => {
@@ -483,4 +513,34 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
+
+ describe('refetchStageData', () => {
+ it('will commit SET_LOADING and dispatch fetchValueStreamStageData actions', () =>
+ testAction({
+ action: actions.refetchStageData,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'SET_LOADING', payload: true },
+ { type: 'SET_LOADING', payload: false },
+ ],
+ expectedActions: [{ type: 'fetchValueStreamStageData' }],
+ }));
+ });
+
+ describe('fetchValueStreamStageData', () => {
+ it('will dispatch the fetchCycleAnalyticsData, fetchStageData, fetchStageMedians and fetchStageCountValues actions', () =>
+ testAction({
+ action: actions.fetchValueStreamStageData,
+ state,
+ payload: {},
+ expectedMutations: [],
+ expectedActions: [
+ { type: 'fetchCycleAnalyticsData' },
+ { type: 'fetchStageData' },
+ { type: 'fetchStageMedians' },
+ { type: 'fetchStageCountValues' },
+ ],
+ }));
+ });
});
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 4860225c995..2670a390e9c 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -101,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }}
${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }}
+ ${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index 74d64cd8d71..a6d6d022781 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,11 +1,11 @@
-import { useFakeDate } from 'helpers/fake_date';
+import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import {
transformStagesForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
- calculateFormattedDayInPast,
prepareTimeMetricsData,
+ buildCycleAnalyticsInitialData,
} from '~/cycle_analytics/utils';
import { slugify } from '~/lib/utils/text_utility';
import {
@@ -14,7 +14,6 @@ import {
stageMedians,
pathNavIssueMetric,
rawStageMedians,
- metricsData,
} from './mock_data';
describe('Value stream analytics utils', () => {
@@ -90,14 +89,6 @@ describe('Value stream analytics utils', () => {
});
});
- describe('calculateFormattedDayInPast', () => {
- useFakeDate(1815, 11, 10);
-
- it('will return 2 dates, now and past', () => {
- expect(calculateFormattedDayInPast(5)).toEqual({ now: '1815-12-10', past: '1815-12-05' });
- });
- });
-
describe('prepareTimeMetricsData', () => {
let prepared;
const [first, second] = metricsData;
@@ -125,4 +116,87 @@ describe('Value stream analytics utils', () => {
]);
});
});
+
+ describe('buildCycleAnalyticsInitialData', () => {
+ let res = null;
+ const projectId = '5';
+ const createdAfter = '2021-09-01';
+ const createdBefore = '2021-11-06';
+ const groupId = '146';
+ const groupPath = 'fake-group';
+ const fullPath = 'fake-group/fake-project';
+ const labelsPath = '/fake-group/fake-project/-/labels.json';
+ const milestonesPath = '/fake-group/fake-project/-/milestones.json';
+ const requestPath = '/fake-group/fake-project/-/value_stream_analytics';
+
+ const rawData = {
+ projectId,
+ createdBefore,
+ createdAfter,
+ fullPath,
+ requestPath,
+ labelsPath,
+ milestonesPath,
+ groupId,
+ groupPath,
+ };
+
+ describe('with minimal data', () => {
+ beforeEach(() => {
+ res = buildCycleAnalyticsInitialData(rawData);
+ });
+
+ it('sets the projectId', () => {
+ expect(res.projectId).toBe(parseInt(projectId, 10));
+ });
+
+ it('sets the date range', () => {
+ expect(res.createdBefore).toEqual(new Date(createdBefore));
+ expect(res.createdAfter).toEqual(new Date(createdAfter));
+ });
+
+ it('sets the endpoints', () => {
+ const { endpoints } = res;
+ expect(endpoints.fullPath).toBe(fullPath);
+ expect(endpoints.requestPath).toBe(requestPath);
+ expect(endpoints.labelsPath).toBe(labelsPath);
+ expect(endpoints.milestonesPath).toBe(milestonesPath);
+ expect(endpoints.groupId).toBe(parseInt(groupId, 10));
+ expect(endpoints.groupPath).toBe(groupPath);
+ });
+
+ it('returns null when there is no stage', () => {
+ expect(res.selectedStage).toBeNull();
+ });
+
+ it('returns false for missing features', () => {
+ expect(res.features.cycleAnalyticsForGroups).toBe(false);
+ });
+ });
+
+ describe('with a stage set', () => {
+ const jsonStage = '{"id":"fakeStage","title":"fakeStage"}';
+
+ it('parses the selectedStage data', () => {
+ res = buildCycleAnalyticsInitialData({ ...rawData, stage: jsonStage });
+
+ const { selectedStage: stage } = res;
+
+ expect(stage.id).toBe('fakeStage');
+ expect(stage.title).toBe('fakeStage');
+ });
+ });
+
+ describe('with features set', () => {
+ const fakeFeatures = { cycleAnalyticsForGroups: true };
+
+ it('sets the feature flags', () => {
+ res = buildCycleAnalyticsInitialData({
+ ...rawData,
+ gon: { licensed_features: fakeFeatures },
+ });
+ expect(res.features).toEqual(fakeFeatures);
+ });
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index ffdb49a828c..c97e4845bc2 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -1,13 +1,16 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
+import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import waitForPromises from 'helpers/wait_for_promises';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import createFlash from '~/flash';
-import { group, metricsData } from './mock_data';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { group } from './mock_data';
jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility');
describe('ValueStreamMetrics', () => {
let wrapper;
@@ -43,7 +46,6 @@ describe('ValueStreamMetrics', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('with successful requests', () => {
@@ -55,7 +57,23 @@ describe('ValueStreamMetrics', () => {
it('will display a loader with pending requests', async () => {
await wrapper.vm.$nextTick();
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
+ });
+
+ it('renders hidden GlSingleStat components for each metric', async () => {
+ await waitForPromises();
+
+ wrapper.setData({ isLoading: true });
+
+ await wrapper.vm.$nextTick();
+
+ const components = findMetrics();
+
+ expect(components).toHaveLength(metricsData.length);
+
+ metricsData.forEach((metric, index) => {
+ expect(components.at(index).isVisible()).toBe(false);
+ });
});
describe('with data loaded', () => {
@@ -67,19 +85,31 @@ describe('ValueStreamMetrics', () => {
expectToHaveRequest({ params: {} });
});
- it.each`
- index | value | title | unit
- ${0} | ${metricsData[0].value} | ${metricsData[0].title} | ${metricsData[0].unit}
- ${1} | ${metricsData[1].value} | ${metricsData[1].title} | ${metricsData[1].unit}
- ${2} | ${metricsData[2].value} | ${metricsData[2].title} | ${metricsData[2].unit}
- ${3} | ${metricsData[3].value} | ${metricsData[3].title} | ${metricsData[3].unit}
- `(
- 'renders a single stat component for the $title with value and unit',
- ({ index, value, title, unit }) => {
+ describe.each`
+ index | value | title | unit | clickable
+ ${0} | ${metricsData[0].value} | ${metricsData[0].title} | ${metricsData[0].unit} | ${false}
+ ${1} | ${metricsData[1].value} | ${metricsData[1].title} | ${metricsData[1].unit} | ${false}
+ ${2} | ${metricsData[2].value} | ${metricsData[2].title} | ${metricsData[2].unit} | ${false}
+ ${3} | ${metricsData[3].value} | ${metricsData[3].title} | ${metricsData[3].unit} | ${true}
+ `('metric tiles', ({ index, value, title, unit, clickable }) => {
+ it(`renders a single stat component for "${title}" with value and unit`, () => {
const metric = findMetrics().at(index);
expect(metric.props()).toMatchObject({ value, title, unit: unit ?? '' });
- },
- );
+ expect(metric.isVisible()).toBe(true);
+ });
+
+ it(`${
+ clickable ? 'redirects' : "doesn't redirect"
+ } when the user clicks the "${title}" metric`, () => {
+ const metric = findMetrics().at(index);
+ metric.vm.$emit('click');
+ if (clickable) {
+ expect(redirectTo).toHaveBeenCalledWith(metricsData[index].links[0].url);
+ } else {
+ expect(redirectTo).not.toHaveBeenCalled();
+ }
+ });
+ });
it('will not display a loading icon', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
diff --git a/spec/frontend/delete_label_modal_spec.js b/spec/frontend/delete_label_modal_spec.js
index df70d3a8393..0b3e6fe652a 100644
--- a/spec/frontend/delete_label_modal_spec.js
+++ b/spec/frontend/delete_label_modal_spec.js
@@ -40,7 +40,7 @@ describe('DeleteLabelModal', () => {
it('starts with only js-containers', () => {
expect(findJsHooks()).toHaveLength(buttons.length);
- expect(findModal()).not.toExist();
+ expect(findModal()).toBe(null);
});
describe('when first button clicked', () => {
@@ -54,7 +54,7 @@ describe('DeleteLabelModal', () => {
});
it('renders GlModal', () => {
- expect(findModal()).toExist();
+ expect(findModal()).not.toBe(null);
});
});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 511b9d6ef55..51c120d8213 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -50,20 +50,20 @@ describe('Deploy keys key', () => {
it('shows pencil button for editing', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="pencil-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="pencil-icon"]').exists()).toBe(true);
});
it('shows disable button when the project is not deletable', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="cancel-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="cancel-icon"]').exists()).toBe(true);
});
it('shows remove button when the project is deletable', () => {
createComponent({
deployKey: { ...deployKey, destroyed_when_orphaned: true, almost_orphaned: true },
});
- expect(wrapper.find('.btn [data-testid="remove-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="remove-icon"]').exists()).toBe(true);
});
});
@@ -137,7 +137,7 @@ describe('Deploy keys key', () => {
it('shows pencil button for editing', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="pencil-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="pencil-icon"]').exists()).toBe(true);
});
it('shows disable button when key is enabled', () => {
@@ -145,7 +145,7 @@ describe('Deploy keys key', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="cancel-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="cancel-icon"]').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
index f3b907e5450..f5f76d5d493 100644
--- a/spec/frontend/deploy_keys/components/keys_panel_spec.js
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -37,7 +37,7 @@ describe('Deploy keys panel', () => {
mountComponent();
const tableHeader = findTableRowHeader();
- expect(tableHeader).toExist();
+ expect(tableHeader.exists()).toBe(true);
expect(tableHeader.text()).toContain('Deploy key');
expect(tableHeader.text()).toContain('Project usage');
expect(tableHeader.text()).toContain('Created');
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 7e4c6e131b4..bec91fe5fc5 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -1,10 +1,9 @@
/* eslint-disable no-param-reassign */
import $ from 'jquery';
+import mockProjects from 'test_fixtures_static/projects.json';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import '~/lib/utils/common_utils';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -68,8 +67,7 @@ describe('deprecatedJQueryDropdown', () => {
loadFixtures('static/deprecated_jquery_dropdown.html');
test.dropdownContainerElement = $('.dropdown.inline');
test.$dropdownMenuElement = $('.dropdown-menu', test.dropdownContainerElement);
- // eslint-disable-next-line import/no-deprecated
- test.projectsData = getJSONFixture('static/projects.json');
+ test.projectsData = JSON.parse(JSON.stringify(mockProjects));
});
afterEach(() => {
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 58636ece91e..ed105b112be 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -87,7 +87,7 @@ describe('Design management list item component', () => {
describe('before image is loaded', () => {
it('renders loading spinner', () => {
- expect(wrapper.find(GlLoadingIcon)).toExist();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index ce79feae2e7..427161a391b 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -669,6 +669,20 @@ describe('Design management index page', () => {
expect(variables.files).toEqual(event.clipboardData.files.map((f) => new File([f], '')));
});
+ it('display original file name', () => {
+ event.clipboardData.files = [new File([new Blob()], 'test.png', { type: 'image/png' })];
+ document.dispatchEvent(event);
+
+ const [{ mutation, variables }] = mockMutate.mock.calls[0];
+ expect(mutation).toBe(uploadDesignMutation);
+ expect(variables).toStrictEqual({
+ files: expect.any(Array),
+ iid: '1',
+ projectPath: 'project-path',
+ });
+ expect(variables.files[0].name).toEqual('test.png');
+ });
+
it('renames a design if it has an image.png filename', () => {
event.clipboardData.getData = () => 'image.png';
document.dispatchEvent(event);
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 0527c2153f4..d50ac0529d6 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -388,15 +388,24 @@ describe('diffs/components/app', () => {
wrapper.vm.jumpToFile(+1);
- expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['diffs/scrollToFile', '222.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual([
+ 'diffs/scrollToFile',
+ { path: '222.js' },
+ ]);
store.state.diffs.currentDiffFileId = '222';
wrapper.vm.jumpToFile(+1);
- expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['diffs/scrollToFile', '333.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual([
+ 'diffs/scrollToFile',
+ { path: '333.js' },
+ ]);
store.state.diffs.currentDiffFileId = '333';
wrapper.vm.jumpToFile(-1);
- expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['diffs/scrollToFile', '222.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual([
+ 'diffs/scrollToFile',
+ { path: '222.js' },
+ ]);
});
it('does not jump to previous file from the first one', async () => {
@@ -702,23 +711,4 @@ describe('diffs/components/app', () => {
);
});
});
-
- describe('fluid layout', () => {
- beforeEach(() => {
- setFixtures(
- '<div><div class="merge-request-container limit-container-width container-limited"></div></div>',
- );
- });
-
- it('removes limited container classes when on diffs tab', () => {
- createComponent({ isFluidLayout: false, shouldShow: true }, () => {}, {
- glFeatures: { mrChangesFluidLayout: true },
- });
-
- const containerClassList = document.querySelector('.merge-request-container').classList;
-
- expect(containerClassList).not.toContain('container-limited');
- expect(containerClassList).not.toContain('limit-container-width');
- });
- });
});
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index bd6f4cd2545..c847a79435a 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -1,6 +1,7 @@
import { GlIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import { createStore } from '~/mr_notes/stores';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
@@ -19,6 +20,9 @@ describe('DiffDiscussions', () => {
store = createStore();
wrapper = mount(localVue.extend(DiffDiscussions), {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: {
discussions: getDiscussionsMockData(),
...props,
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index b16ef8fe6b0..342b4bfcc50 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -7,7 +7,7 @@ import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '~/diffs/constants';
import { reviewFile } from '~/diffs/store/actions';
-import { SET_MR_FILE_REVIEWS } from '~/diffs/store/mutation_types';
+import { SET_DIFF_FILE_VIEWED, SET_MR_FILE_REVIEWS } from '~/diffs/store/mutation_types';
import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -23,6 +23,7 @@ jest.mock('~/lib/utils/common_utils');
const diffFile = Object.freeze(
Object.assign(diffDiscussionsMockData.diff_file, {
id: '123',
+ file_hash: 'xyz',
file_identifier_hash: 'abc',
edit_path: 'link:/to/edit/path',
blob: {
@@ -58,7 +59,7 @@ describe('DiffFileHeader component', () => {
toggleFileDiscussions: jest.fn(),
toggleFileDiscussionWrappers: jest.fn(),
toggleFullDiff: jest.fn(),
- toggleActiveFileByHash: jest.fn(),
+ setCurrentFileHash: jest.fn(),
setFileCollapsedByUser: jest.fn(),
reviewFile: jest.fn(),
},
@@ -240,18 +241,19 @@ describe('DiffFileHeader component', () => {
});
describe('for any file', () => {
- const otherModes = Object.keys(diffViewerModes).filter((m) => m !== 'mode_changed');
+ const allModes = Object.keys(diffViewerModes).map((m) => [m]);
- it('for mode_changed file mode displays mode changes', () => {
+ it.each(allModes)('for %s file mode displays mode changes', (mode) => {
createComponent({
props: {
diffFile: {
...diffFile,
+ mode_changed: true,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
...diffFile.viewer,
- name: diffViewerModes.mode_changed,
+ name: diffViewerModes[mode],
},
},
},
@@ -259,13 +261,14 @@ describe('DiffFileHeader component', () => {
expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/);
});
- it.each(otherModes.map((m) => [m]))(
+ it.each(allModes.filter((m) => m[0] !== 'mode_changed'))(
'for %s file mode does not display mode changes',
(mode) => {
createComponent({
props: {
diffFile: {
...diffFile,
+ mode_changed: false,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
@@ -553,7 +556,13 @@ describe('DiffFileHeader component', () => {
reviewFile,
{ file, reviewed: true },
{},
- [{ type: SET_MR_FILE_REVIEWS, payload: { [file.file_identifier_hash]: [file.id] } }],
+ [
+ { type: SET_DIFF_FILE_VIEWED, payload: { id: file.file_hash, seen: true } },
+ {
+ type: SET_MR_FILE_REVIEWS,
+ payload: { [file.file_identifier_hash]: [file.id, `hash:${file.file_hash}`] },
+ },
+ ],
[],
);
});
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index a192f7e2e9a..0ccf996e220 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,10 +1,18 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import { createStore } from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { noteableDataMock } from '../../notes/mock_data';
import diffFileMockData from '../mock_data/diff_file';
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
+ return {
+ confirmAction: jest.fn(),
+ };
+});
+
describe('DiffLineNoteForm', () => {
let wrapper;
let diffFile;
@@ -24,57 +32,68 @@ describe('DiffLineNoteForm', () => {
return shallowMount(DiffLineNoteForm, {
store,
propsData: {
- diffFileHash: diffFile.file_hash,
- diffLines,
- line: diffLines[0],
- noteTargetLine: diffLines[0],
+ ...{
+ diffFileHash: diffFile.file_hash,
+ diffLines,
+ line: diffLines[1],
+ range: { start: diffLines[0], end: diffLines[1] },
+ noteTargetLine: diffLines[1],
+ },
+ ...(args.props || {}),
},
});
};
+ const findNoteForm = () => wrapper.findComponent(NoteForm);
+
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('handleCancelCommentForm', () => {
+ afterEach(() => {
+ confirmAction.mockReset();
+ });
+
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
- jest.spyOn(window, 'confirm').mockReturnValue(false);
+ confirmAction.mockResolvedValueOnce(false);
- wrapper.vm.handleCancelCommentForm(true, true);
+ findNoteForm().vm.$emit('cancelForm', true, true);
- expect(window.confirm).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalled();
});
- it('should ask for confirmation when one of the params false', () => {
- jest.spyOn(window, 'confirm').mockReturnValue(false);
+ it('should not ask for confirmation when one of the params false', () => {
+ confirmAction.mockResolvedValueOnce(false);
- wrapper.vm.handleCancelCommentForm(true, false);
+ findNoteForm().vm.$emit('cancelForm', true, false);
- expect(window.confirm).not.toHaveBeenCalled();
+ expect(confirmAction).not.toHaveBeenCalled();
- wrapper.vm.handleCancelCommentForm(false, true);
+ findNoteForm().vm.$emit('cancelForm', false, true);
- expect(window.confirm).not.toHaveBeenCalled();
+ expect(confirmAction).not.toHaveBeenCalled();
});
- it('should call cancelCommentForm with lineCode', (done) => {
- jest.spyOn(window, 'confirm').mockImplementation(() => {});
+ it('should call cancelCommentForm with lineCode', async () => {
+ confirmAction.mockResolvedValueOnce(true);
jest.spyOn(wrapper.vm, 'cancelCommentForm').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'resetAutoSave').mockImplementation(() => {});
- wrapper.vm.handleCancelCommentForm();
- expect(window.confirm).not.toHaveBeenCalled();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
- lineCode: diffLines[0].line_code,
- fileHash: wrapper.vm.diffFileHash,
- });
+ findNoteForm().vm.$emit('cancelForm', true, true);
+
+ await nextTick();
+
+ expect(confirmAction).toHaveBeenCalled();
- expect(wrapper.vm.resetAutoSave).toHaveBeenCalled();
+ await nextTick();
- done();
+ expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
+ lineCode: diffLines[1].line_code,
+ fileHash: wrapper.vm.diffFileHash,
});
+ expect(wrapper.vm.resetAutoSave).toHaveBeenCalled();
});
});
@@ -88,13 +107,13 @@ describe('DiffLineNoteForm', () => {
start: {
line_code: wrapper.vm.commentLineStart.line_code,
type: wrapper.vm.commentLineStart.type,
- new_line: 1,
+ new_line: 2,
old_line: null,
},
end: {
line_code: wrapper.vm.line.line_code,
type: wrapper.vm.line.type,
- new_line: 1,
+ new_line: 2,
old_line: null,
},
};
@@ -118,9 +137,25 @@ describe('DiffLineNoteForm', () => {
});
});
+ describe('created', () => {
+ it('should use the provided `range` of lines', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.vm.lines.start).toBe(diffLines[0]);
+ expect(wrapper.vm.lines.end).toBe(diffLines[1]);
+ });
+
+ it("should fill the internal `lines` data with the provided `line` if there's no provided `range", () => {
+ wrapper = createComponent({ props: { range: null } });
+
+ expect(wrapper.vm.lines.start).toBe(diffLines[1]);
+ expect(wrapper.vm.lines.end).toBe(diffLines[1]);
+ });
+ });
+
describe('mounted', () => {
it('should init autosave', () => {
- const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
+ const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2';
wrapper = createComponent();
expect(wrapper.vm.autosave).toBeDefined();
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index f316a9fdf01..31044b0818c 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -113,7 +113,9 @@ describe('Diffs tree list component', () => {
wrapper.find('.file-row').trigger('click');
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'app/index.js');
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
+ path: 'app/index.js',
+ });
});
it('renders as file list when renderTreeList is false', () => {
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 85734e05aeb..b5003a54917 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -99,6 +99,10 @@ describe('DiffsStoreActions', () => {
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
const showSuggestPopover = false;
+ const mrReviews = {
+ a: ['z', 'hash:a'],
+ b: ['y', 'hash:a'],
+ };
testAction(
setBaseConfig,
@@ -110,6 +114,7 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
+ mrReviews,
},
{
endpoint: '',
@@ -131,8 +136,21 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
+ mrReviews,
},
},
+ {
+ type: types.SET_DIFF_FILE_VIEWED,
+ payload: { id: 'z', seen: true },
+ },
+ {
+ type: types.SET_DIFF_FILE_VIEWED,
+ payload: { id: 'a', seen: true },
+ },
+ {
+ type: types.SET_DIFF_FILE_VIEWED,
+ payload: { id: 'y', seen: true },
+ },
],
[],
done,
@@ -190,10 +208,10 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
- { type: types.VIEW_DIFF_FILE, payload: 'test' },
+ { type: types.SET_CURRENT_DIFF_FILE, payload: 'test' },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
- { type: types.VIEW_DIFF_FILE, payload: 'test2' },
+ { type: types.SET_CURRENT_DIFF_FILE, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
],
@@ -307,7 +325,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.VIEW_DIFF_FILE, payload: 'ABC' },
+ { type: types.SET_CURRENT_DIFF_FILE, payload: 'ABC' },
]);
});
});
@@ -890,12 +908,12 @@ describe('DiffsStoreActions', () => {
},
};
- scrollToFile({ state, commit, getters }, 'path');
+ scrollToFile({ state, commit, getters }, { path: 'path' });
expect(document.location.hash).toBe('#test');
});
- it('commits VIEW_DIFF_FILE', () => {
+ it('commits SET_CURRENT_DIFF_FILE', () => {
const state = {
treeEntries: {
path: {
@@ -904,9 +922,9 @@ describe('DiffsStoreActions', () => {
},
};
- scrollToFile({ state, commit, getters }, 'path');
+ scrollToFile({ state, commit, getters }, { path: 'path' });
- expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, 'test');
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, 'test');
});
});
@@ -1428,7 +1446,7 @@ describe('DiffsStoreActions', () => {
});
describe('setCurrentDiffFileIdFromNote', () => {
- it('commits VIEW_DIFF_FILE', () => {
+ it('commits SET_CURRENT_DIFF_FILE', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1438,10 +1456,10 @@ describe('DiffsStoreActions', () => {
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
- expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, '123');
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, '123');
});
- it('does not commit VIEW_DIFF_FILE when discussion has no diff_file', () => {
+ it('does not commit SET_CURRENT_DIFF_FILE when discussion has no diff_file', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1454,7 +1472,7 @@ describe('DiffsStoreActions', () => {
expect(commit).not.toHaveBeenCalled();
});
- it('does not commit VIEW_DIFF_FILE when diff file does not exist', () => {
+ it('does not commit SET_CURRENT_DIFF_FILE when diff file does not exist', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1469,12 +1487,12 @@ describe('DiffsStoreActions', () => {
});
describe('navigateToDiffFileIndex', () => {
- it('commits VIEW_DIFF_FILE', (done) => {
+ it('commits SET_CURRENT_DIFF_FILE', (done) => {
testAction(
navigateToDiffFileIndex,
0,
{ diffFiles: [{ file_hash: '123' }] },
- [{ type: types.VIEW_DIFF_FILE, payload: '123' }],
+ [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
done,
);
@@ -1523,13 +1541,14 @@ describe('DiffsStoreActions', () => {
describe('reviewFile', () => {
const file = {
id: '123',
+ file_hash: 'xyz',
file_identifier_hash: 'abc',
load_collapsed_diff_url: 'gitlab-org/gitlab-test/-/merge_requests/1/diffs',
};
it.each`
- reviews | diffFile | reviewed
- ${{ abc: ['123'] }} | ${file} | ${true}
- ${{}} | ${file} | ${false}
+ reviews | diffFile | reviewed
+ ${{ abc: ['123', 'hash:xyz'] }} | ${file} | ${true}
+ ${{}} | ${file} | ${false}
`(
'sets reviews ($reviews) to localStorage and state for file $file if it is marked reviewed=$reviewed',
({ reviews, diffFile, reviewed }) => {
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index fc9ba223d5a..c104fcd5fb9 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -633,16 +633,36 @@ describe('DiffsStoreMutations', () => {
});
});
- describe('VIEW_DIFF_FILE', () => {
+ describe('SET_CURRENT_DIFF_FILE', () => {
it('updates currentDiffFileId', () => {
const state = createState();
- mutations[types.VIEW_DIFF_FILE](state, 'somefileid');
+ mutations[types.SET_CURRENT_DIFF_FILE](state, 'somefileid');
expect(state.currentDiffFileId).toBe('somefileid');
});
});
+ describe('SET_DIFF_FILE_VIEWED', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ viewedDiffFileIds: { 123: true },
+ };
+ });
+
+ it.each`
+ id | bool | outcome
+ ${'abc'} | ${true} | ${{ 123: true, abc: true }}
+ ${'123'} | ${false} | ${{ 123: false }}
+ `('sets the viewed files list to $bool for the id $id', ({ id, bool, outcome }) => {
+ mutations[types.SET_DIFF_FILE_VIEWED](state, { id, seen: bool });
+
+ expect(state.viewedDiffFileIds).toEqual(outcome);
+ });
+ });
+
describe('Set highlighted row', () => {
it('sets highlighted row', () => {
const state = createState();
diff --git a/spec/frontend/diffs/utils/diff_line_spec.js b/spec/frontend/diffs/utils/diff_line_spec.js
new file mode 100644
index 00000000000..adcb4a4433c
--- /dev/null
+++ b/spec/frontend/diffs/utils/diff_line_spec.js
@@ -0,0 +1,30 @@
+import { pickDirection } from '~/diffs/utils/diff_line';
+
+describe('diff_line utilities', () => {
+ describe('pickDirection', () => {
+ const left = {
+ line_code: 'left',
+ };
+ const right = {
+ line_code: 'right',
+ };
+ const defaultLine = {
+ left,
+ right,
+ };
+
+ it.each`
+ code | pick | line | pickDescription
+ ${'left'} | ${left} | ${defaultLine} | ${'the left line'}
+ ${'right'} | ${right} | ${defaultLine} | ${'the right line'}
+ ${'junk'} | ${left} | ${defaultLine} | ${'the default: the left line'}
+ ${'junk'} | ${right} | ${{ right }} | ${"the right line if there's no left line to default to"}
+ ${'right'} | ${left} | ${{ left }} | ${"the left line when there isn't a right line to match"}
+ `(
+ 'when provided a line and a line code `$code`, picks $pickDescription',
+ ({ code, line, pick }) => {
+ expect(pickDirection({ line, code })).toBe(pick);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/diffs/utils/discussions_spec.js b/spec/frontend/diffs/utils/discussions_spec.js
new file mode 100644
index 00000000000..9a3d442d943
--- /dev/null
+++ b/spec/frontend/diffs/utils/discussions_spec.js
@@ -0,0 +1,133 @@
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
+
+describe('Diff Discussions Utils', () => {
+ describe('discussionIntersectionObserverHandlerFactory', () => {
+ it('creates a handler function', () => {
+ expect(discussionIntersectionObserverHandlerFactory()).toBeInstanceOf(Function);
+ });
+
+ describe('intersection observer handler', () => {
+ const functions = {
+ setCurrentDiscussionId: jest.fn(),
+ getPreviousUnresolvedDiscussionId: jest.fn().mockImplementation((id) => {
+ return Number(id) - 1;
+ }),
+ };
+ const defaultProcessableWrapper = {
+ entry: {
+ time: 0,
+ isIntersecting: true,
+ rootBounds: {
+ bottom: 0,
+ },
+ boundingClientRect: {
+ top: 0,
+ },
+ },
+ currentDiscussion: {
+ id: 1,
+ },
+ isFirstUnresolved: false,
+ isDiffsPage: true,
+ };
+ let handler;
+ let getMock;
+ let setMock;
+
+ beforeEach(() => {
+ functions.setCurrentDiscussionId.mockClear();
+ functions.getPreviousUnresolvedDiscussionId.mockClear();
+
+ defaultProcessableWrapper.functions = functions;
+
+ setMock = functions.setCurrentDiscussionId.mock;
+ getMock = functions.getPreviousUnresolvedDiscussionId.mock;
+ handler = discussionIntersectionObserverHandlerFactory();
+ });
+
+ it('debounces multiple simultaneous requests into one queue', () => {
+ handler(defaultProcessableWrapper);
+ handler(defaultProcessableWrapper);
+ handler(defaultProcessableWrapper);
+ handler(defaultProcessableWrapper);
+
+ expect(setTimeout).toHaveBeenCalledTimes(4);
+ expect(clearTimeout).toHaveBeenCalledTimes(3);
+
+ // By only advancing to one timer, we ensure it's all being batched into one queue
+ jest.advanceTimersToNextTimer();
+
+ expect(functions.setCurrentDiscussionId).toHaveBeenCalledTimes(4);
+ });
+
+ it('properly processes, sorts and executes the correct actions for a set of observed intersections', () => {
+ handler(defaultProcessableWrapper);
+ handler({
+ // This observation is here to be filtered out because it's a scrollDown
+ ...defaultProcessableWrapper,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ isIntersecting: false,
+ boundingClientRect: { top: 10 },
+ rootBounds: { bottom: 100 },
+ },
+ });
+ handler({
+ ...defaultProcessableWrapper,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ time: 101,
+ isIntersecting: false,
+ rootBounds: { bottom: -100 },
+ },
+ currentDiscussion: { id: 20 },
+ });
+ handler({
+ ...defaultProcessableWrapper,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ time: 100,
+ isIntersecting: false,
+ boundingClientRect: { top: 100 },
+ },
+ currentDiscussion: { id: 30 },
+ isDiffsPage: false,
+ });
+ handler({
+ ...defaultProcessableWrapper,
+ isFirstUnresolved: true,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ time: 100,
+ isIntersecting: false,
+ boundingClientRect: { top: 200 },
+ },
+ });
+
+ jest.advanceTimersToNextTimer();
+
+ expect(setMock.calls.length).toBe(4);
+ expect(setMock.calls[0]).toEqual([1]);
+ expect(setMock.calls[1]).toEqual([29]);
+ expect(setMock.calls[2]).toEqual([null]);
+ expect(setMock.calls[3]).toEqual([19]);
+
+ expect(getMock.calls.length).toBe(2);
+ expect(getMock.calls[0]).toEqual([30, false]);
+ expect(getMock.calls[1]).toEqual([20, true]);
+
+ [
+ setMock.invocationCallOrder[0],
+ getMock.invocationCallOrder[0],
+ setMock.invocationCallOrder[1],
+ setMock.invocationCallOrder[2],
+ getMock.invocationCallOrder[1],
+ setMock.invocationCallOrder[3],
+ ].forEach((order, idx, list) => {
+ // Compare each invocation sequence to the one before it (except the first one)
+ expect(list[idx - 1] || -1).toBeLessThan(order);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/diffs/utils/file_reviews_spec.js b/spec/frontend/diffs/utils/file_reviews_spec.js
index 230ec12409c..ccd27a5ae3e 100644
--- a/spec/frontend/diffs/utils/file_reviews_spec.js
+++ b/spec/frontend/diffs/utils/file_reviews_spec.js
@@ -11,14 +11,14 @@ import {
function getDefaultReviews() {
return {
- abc: ['123', '098'],
+ abc: ['123', 'hash:xyz', '098', 'hash:uvw'],
};
}
describe('File Review(s) utilities', () => {
const mrPath = 'my/fake/mr/42';
const storageKey = `${mrPath}-file-reviews`;
- const file = { id: '123', file_identifier_hash: 'abc' };
+ const file = { id: '123', file_hash: 'xyz', file_identifier_hash: 'abc' };
const storedValue = JSON.stringify(getDefaultReviews());
let reviews;
@@ -44,14 +44,14 @@ describe('File Review(s) utilities', () => {
});
describe('reviewStatuses', () => {
- const file1 = { id: '123', file_identifier_hash: 'abc' };
- const file2 = { id: '098', file_identifier_hash: 'abc' };
+ const file1 = { id: '123', hash: 'xyz', file_identifier_hash: 'abc' };
+ const file2 = { id: '098', hash: 'uvw', file_identifier_hash: 'abc' };
it.each`
mrReviews | files | fileReviews
${{}} | ${[file1, file2]} | ${{ 123: false, '098': false }}
- ${{ abc: ['123'] }} | ${[file1, file2]} | ${{ 123: true, '098': false }}
- ${{ abc: ['098'] }} | ${[file1, file2]} | ${{ 123: false, '098': true }}
+ ${{ abc: ['123', 'hash:xyz'] }} | ${[file1, file2]} | ${{ 123: true, '098': false }}
+ ${{ abc: ['098', 'hash:uvw'] }} | ${[file1, file2]} | ${{ 123: false, '098': true }}
${{ def: ['123'] }} | ${[file1, file2]} | ${{ 123: false, '098': false }}
${{ abc: ['123'], def: ['098'] }} | ${[]} | ${{}}
`(
@@ -128,7 +128,7 @@ describe('File Review(s) utilities', () => {
describe('markFileReview', () => {
it("adds a review when there's nothing that already exists", () => {
- expect(markFileReview(null, file)).toStrictEqual({ abc: ['123'] });
+ expect(markFileReview(null, file)).toStrictEqual({ abc: ['123', 'hash:xyz'] });
});
it("overwrites an existing review if it's for the same file (identifier hash)", () => {
@@ -136,15 +136,15 @@ describe('File Review(s) utilities', () => {
});
it('removes a review from the list when `reviewed` is `false`', () => {
- expect(markFileReview(reviews, file, false)).toStrictEqual({ abc: ['098'] });
+ expect(markFileReview(reviews, file, false)).toStrictEqual({ abc: ['098', 'hash:uvw'] });
});
it('adds a new review if the file ID is new', () => {
- const updatedFile = { ...file, id: '098' };
- const allReviews = markFileReview({ abc: ['123'] }, updatedFile);
+ const updatedFile = { ...file, id: '098', file_hash: 'uvw' };
+ const allReviews = markFileReview({ abc: ['123', 'hash:xyz'] }, updatedFile);
expect(allReviews).toStrictEqual(getDefaultReviews());
- expect(allReviews.abc).toStrictEqual(['123', '098']);
+ expect(allReviews.abc).toStrictEqual(['123', 'hash:xyz', '098', 'hash:uvw']);
});
it.each`
@@ -158,7 +158,7 @@ describe('File Review(s) utilities', () => {
it('removes the file key if there are no more reviews for it', () => {
let updated = markFileReview(reviews, file, false);
- updated = markFileReview(updated, { ...file, id: '098' }, false);
+ updated = markFileReview(updated, { ...file, id: '098', file_hash: 'uvw' }, false);
expect(updated).toStrictEqual({});
});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index acf7d0780cd..12e10f7c5f4 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -71,6 +71,7 @@ describe('dropzone_input', () => {
triggerPasteEvent({
types: ['text/plain', 'text/html', 'text/rtf', 'Files'],
getData: () => longFileName,
+ files: [new File([new Blob()], longFileName, { type: 'image/png' })],
items: [
{
kind: 'file',
@@ -84,6 +85,24 @@ describe('dropzone_input', () => {
await waitForPromises();
expect(axiosMock.history.post[0].data.get('file').name).toHaveLength(246);
});
+
+ it('display original file name in comment box', async () => {
+ const axiosMock = new MockAdapter(axios);
+ triggerPasteEvent({
+ types: ['Files'],
+ files: [new File([new Blob()], 'test.png', { type: 'image/png' })],
+ items: [
+ {
+ kind: 'file',
+ type: 'image/png',
+ getAsFile: () => new Blob(),
+ },
+ ],
+ });
+ axiosMock.onPost().reply(httpStatusCodes.OK, { link: { markdown: 'foo' } });
+ await waitForPromises();
+ expect(axiosMock.history.post[0].data.get('file').name).toEqual('test.png');
+ });
});
describe('shows error message', () => {
diff --git a/spec/frontend/editor/helpers.js b/spec/frontend/editor/helpers.js
new file mode 100644
index 00000000000..6f7cdf6efb3
--- /dev/null
+++ b/spec/frontend/editor/helpers.js
@@ -0,0 +1,53 @@
+export class MyClassExtension {
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ shared: () => 'extension',
+ classExtMethod: () => 'class own method',
+ };
+ }
+}
+
+export function MyFnExtension() {
+ return {
+ fnExtMethod: () => 'fn own method',
+ provides: () => {
+ return {
+ fnExtMethod: () => 'class own method',
+ };
+ },
+ };
+}
+
+export const MyConstExt = () => {
+ return {
+ provides: () => {
+ return {
+ constExtMethod: () => 'const own method',
+ };
+ },
+ };
+};
+
+export const conflictingExtensions = {
+ WithInstanceExt: () => {
+ return {
+ provides: () => {
+ return {
+ use: () => 'A conflict with instance',
+ ownMethod: () => 'Non-conflicting method',
+ };
+ },
+ };
+ },
+ WithAnotherExt: () => {
+ return {
+ provides: () => {
+ return {
+ shared: () => 'A conflict with extension',
+ ownMethod: () => 'Non-conflicting method',
+ };
+ },
+ };
+ },
+};
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 2c06ae03892..a0fb1178b3b 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -148,7 +148,10 @@ describe('The basis for an Source Editor extension', () => {
revealLineInCenter: revealSpy,
deltaDecorations: decorationsSpy,
};
- const defaultDecorationOptions = { isWholeLine: true, className: 'active-line-text' };
+ const defaultDecorationOptions = {
+ isWholeLine: true,
+ className: 'active-line-text',
+ };
useFakeRequestAnimationFrame();
@@ -157,18 +160,22 @@ describe('The basis for an Source Editor extension', () => {
});
it.each`
- desc | hash | shouldReveal | expectedRange
- ${'properly decorates a single line'} | ${'#L10'} | ${true} | ${[10, 1, 10, 1]}
- ${'properly decorates multiple lines'} | ${'#L7-42'} | ${true} | ${[7, 1, 42, 1]}
- ${'correctly highlights if lines are reversed'} | ${'#L42-7'} | ${true} | ${[7, 1, 42, 1]}
- ${'highlights one line if start/end are the same'} | ${'#L7-7'} | ${true} | ${[7, 1, 7, 1]}
- ${'does not highlight if there is no hash'} | ${''} | ${false} | ${null}
- ${'does not highlight if the hash is undefined'} | ${undefined} | ${false} | ${null}
- ${'does not highlight if hash is incomplete 1'} | ${'#L'} | ${false} | ${null}
- ${'does not highlight if hash is incomplete 2'} | ${'#L-'} | ${false} | ${null}
- `('$desc', ({ hash, shouldReveal, expectedRange } = {}) => {
+ desc | hash | bounds | shouldReveal | expectedRange
+ ${'properly decorates a single line'} | ${'#L10'} | ${undefined} | ${true} | ${[10, 1, 10, 1]}
+ ${'properly decorates multiple lines'} | ${'#L7-42'} | ${undefined} | ${true} | ${[7, 1, 42, 1]}
+ ${'correctly highlights if lines are reversed'} | ${'#L42-7'} | ${undefined} | ${true} | ${[7, 1, 42, 1]}
+ ${'highlights one line if start/end are the same'} | ${'#L7-7'} | ${undefined} | ${true} | ${[7, 1, 7, 1]}
+ ${'does not highlight if there is no hash'} | ${''} | ${undefined} | ${false} | ${null}
+ ${'does not highlight if the hash is undefined'} | ${undefined} | ${undefined} | ${false} | ${null}
+ ${'does not highlight if hash is incomplete 1'} | ${'#L'} | ${undefined} | ${false} | ${null}
+ ${'does not highlight if hash is incomplete 2'} | ${'#L-'} | ${undefined} | ${false} | ${null}
+ ${'highlights lines if bounds are passed'} | ${undefined} | ${[17, 42]} | ${true} | ${[17, 1, 42, 1]}
+ ${'highlights one line if bounds has a single value'} | ${undefined} | ${[17]} | ${true} | ${[17, 1, 17, 1]}
+ ${'does not highlight if bounds is invalid'} | ${undefined} | ${[Number.NaN]} | ${false} | ${null}
+ ${'uses bounds if both hash and bounds exist'} | ${'#L7-42'} | ${[3, 5]} | ${true} | ${[3, 1, 5, 1]}
+ `('$desc', ({ hash, bounds, shouldReveal, expectedRange } = {}) => {
window.location.hash = hash;
- SourceEditorExtension.highlightLines(instance);
+ SourceEditorExtension.highlightLines(instance, bounds);
if (!shouldReveal) {
expect(revealSpy).not.toHaveBeenCalled();
expect(decorationsSpy).not.toHaveBeenCalled();
@@ -193,6 +200,43 @@ describe('The basis for an Source Editor extension', () => {
SourceEditorExtension.highlightLines(instance);
expect(instance.lineDecorations).toBe('foo');
});
+
+ it('replaces existing line highlights', () => {
+ const oldLineDecorations = [
+ {
+ range: new Range(1, 1, 20, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ];
+ const newLineDecorations = [
+ {
+ range: new Range(7, 1, 10, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ];
+ instance.lineDecorations = oldLineDecorations;
+ SourceEditorExtension.highlightLines(instance, [7, 10]);
+ expect(decorationsSpy).toHaveBeenCalledWith(oldLineDecorations, newLineDecorations);
+ });
+ });
+
+ describe('removeHighlights', () => {
+ const decorationsSpy = jest.fn();
+ const lineDecorations = [
+ {
+ range: new Range(1, 1, 20, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ];
+ const instance = {
+ deltaDecorations: decorationsSpy,
+ lineDecorations,
+ };
+
+ it('removes all existing decorations', () => {
+ SourceEditorExtension.removeHighlights(instance);
+ expect(decorationsSpy).toHaveBeenCalledWith(lineDecorations, []);
+ });
});
describe('setupLineLinking', () => {
diff --git a/spec/frontend/editor/source_editor_extension_spec.js b/spec/frontend/editor/source_editor_extension_spec.js
new file mode 100644
index 00000000000..6f2eb07a043
--- /dev/null
+++ b/spec/frontend/editor/source_editor_extension_spec.js
@@ -0,0 +1,65 @@
+import EditorExtension from '~/editor/source_editor_extension';
+import { EDITOR_EXTENSION_DEFINITION_ERROR } from '~/editor/constants';
+import * as helpers from './helpers';
+
+describe('Editor Extension', () => {
+ const dummyObj = { foo: 'bar' };
+
+ it.each`
+ definition | setupOptions
+ ${undefined} | ${undefined}
+ ${undefined} | ${{}}
+ ${undefined} | ${dummyObj}
+ ${{}} | ${dummyObj}
+ ${dummyObj} | ${dummyObj}
+ `(
+ 'throws when definition = $definition and setupOptions = $setupOptions',
+ ({ definition, setupOptions }) => {
+ const constructExtension = () => new EditorExtension({ definition, setupOptions });
+ expect(constructExtension).toThrowError(EDITOR_EXTENSION_DEFINITION_ERROR);
+ },
+ );
+
+ it.each`
+ definition | setupOptions | expectedName
+ ${helpers.MyClassExtension} | ${undefined} | ${'MyClassExtension'}
+ ${helpers.MyClassExtension} | ${{}} | ${'MyClassExtension'}
+ ${helpers.MyClassExtension} | ${dummyObj} | ${'MyClassExtension'}
+ ${helpers.MyFnExtension} | ${undefined} | ${'MyFnExtension'}
+ ${helpers.MyFnExtension} | ${{}} | ${'MyFnExtension'}
+ ${helpers.MyFnExtension} | ${dummyObj} | ${'MyFnExtension'}
+ ${helpers.MyConstExt} | ${undefined} | ${'MyConstExt'}
+ ${helpers.MyConstExt} | ${{}} | ${'MyConstExt'}
+ ${helpers.MyConstExt} | ${dummyObj} | ${'MyConstExt'}
+ `(
+ 'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
+ ({ definition, setupOptions, expectedName }) => {
+ const extension = new EditorExtension({ definition, setupOptions });
+ // eslint-disable-next-line new-cap
+ const constructedDefinition = new definition();
+
+ expect(extension).toEqual(
+ expect.objectContaining({
+ name: expectedName,
+ setupOptions,
+ }),
+ );
+ expect(extension.obj.constructor.prototype).toBe(constructedDefinition.constructor.prototype);
+ },
+ );
+
+ describe('api', () => {
+ it.each`
+ definition | expectedKeys
+ ${helpers.MyClassExtension} | ${['shared', 'classExtMethod']}
+ ${helpers.MyFnExtension} | ${['fnExtMethod']}
+ ${helpers.MyConstExt} | ${['constExtMethod']}
+ `('correctly returns API for $definition', ({ definition, expectedKeys }) => {
+ const extension = new EditorExtension({ definition });
+ const expectedApi = Object.fromEntries(
+ expectedKeys.map((key) => [key, expect.any(Function)]),
+ );
+ expect(extension.api).toEqual(expect.objectContaining(expectedApi));
+ });
+ });
+});
diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js
new file mode 100644
index 00000000000..87b20a4ba73
--- /dev/null
+++ b/spec/frontend/editor/source_editor_instance_spec.js
@@ -0,0 +1,387 @@
+import { editor as monacoEditor } from 'monaco-editor';
+import {
+ EDITOR_EXTENSION_NAMING_CONFLICT_ERROR,
+ EDITOR_EXTENSION_NO_DEFINITION_ERROR,
+ EDITOR_EXTENSION_DEFINITION_TYPE_ERROR,
+ EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
+ EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
+} from '~/editor/constants';
+import Instance from '~/editor/source_editor_instance';
+import { sprintf } from '~/locale';
+import { MyClassExtension, conflictingExtensions, MyFnExtension, MyConstExt } from './helpers';
+
+describe('Source Editor Instance', () => {
+ let seInstance;
+
+ const defSetupOptions = { foo: 'bar' };
+ const fullExtensionsArray = [
+ { definition: MyClassExtension },
+ { definition: MyFnExtension },
+ { definition: MyConstExt },
+ ];
+ const fullExtensionsArrayWithOptions = [
+ { definition: MyClassExtension, setupOptions: defSetupOptions },
+ { definition: MyFnExtension, setupOptions: defSetupOptions },
+ { definition: MyConstExt, setupOptions: defSetupOptions },
+ ];
+
+ const fooFn = jest.fn();
+ class DummyExt {
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ fooFn,
+ };
+ }
+ }
+
+ afterEach(() => {
+ seInstance = undefined;
+ });
+
+ it('sets up the registry for the methods coming from extensions', () => {
+ seInstance = new Instance();
+ expect(seInstance.methods).toBeDefined();
+
+ seInstance.use({ definition: MyClassExtension });
+ expect(seInstance.methods).toEqual({
+ shared: 'MyClassExtension',
+ classExtMethod: 'MyClassExtension',
+ });
+
+ seInstance.use({ definition: MyFnExtension });
+ expect(seInstance.methods).toEqual({
+ shared: 'MyClassExtension',
+ classExtMethod: 'MyClassExtension',
+ fnExtMethod: 'MyFnExtension',
+ });
+ });
+
+ describe('proxy', () => {
+ it('returns prop from an extension if extension provides it', () => {
+ seInstance = new Instance();
+ seInstance.use({ definition: DummyExt });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+
+ it('returns props from SE instance itself if no extension provides the prop', () => {
+ seInstance = new Instance({
+ use: fooFn,
+ });
+ jest.spyOn(seInstance, 'use').mockImplementation(() => {});
+ expect(seInstance.use).not.toHaveBeenCalled();
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.use();
+ expect(seInstance.use).toHaveBeenCalled();
+ expect(fooFn).not.toHaveBeenCalled();
+ });
+
+ it('returns props from Monaco instance when the prop does not exist on the SE instance', () => {
+ seInstance = new Instance({
+ fooFn,
+ });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+ });
+
+ describe('public API', () => {
+ it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
+ seInstance = new Instance();
+ expect(seInstance[method]).toBeDefined();
+ });
+
+ describe('use', () => {
+ it('extends the SE instance with methods provided by an extension', () => {
+ seInstance = new Instance();
+ seInstance.use({ definition: DummyExt });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+
+ it.each`
+ extensions | expectedProps
+ ${{ definition: MyClassExtension }} | ${['shared', 'classExtMethod']}
+ ${{ definition: MyFnExtension }} | ${['fnExtMethod']}
+ ${{ definition: MyConstExt }} | ${['constExtMethod']}
+ ${fullExtensionsArray} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
+ ${fullExtensionsArrayWithOptions} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
+ `(
+ 'Should register $expectedProps when extension is "$extensions"',
+ ({ extensions, expectedProps }) => {
+ seInstance = new Instance();
+ expect(seInstance.extensionsAPI).toHaveLength(0);
+
+ seInstance.use(extensions);
+
+ expect(seInstance.extensionsAPI).toEqual(expectedProps);
+ },
+ );
+
+ it.each`
+ definition | preInstalledExtDefinition | expectedErrorProp
+ ${conflictingExtensions.WithInstanceExt} | ${MyClassExtension} | ${'use'}
+ ${conflictingExtensions.WithInstanceExt} | ${null} | ${'use'}
+ ${conflictingExtensions.WithAnotherExt} | ${null} | ${undefined}
+ ${conflictingExtensions.WithAnotherExt} | ${MyClassExtension} | ${'shared'}
+ ${MyClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
+ `(
+ 'logs the naming conflict error when registering $definition',
+ ({ definition, preInstalledExtDefinition, expectedErrorProp }) => {
+ seInstance = new Instance();
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ if (preInstalledExtDefinition) {
+ seInstance.use({ definition: preInstalledExtDefinition });
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled();
+ }
+
+ seInstance.use({ definition });
+
+ if (expectedErrorProp) {
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.stringContaining(
+ sprintf(EDITOR_EXTENSION_NAMING_CONFLICT_ERROR, { prop: expectedErrorProp }),
+ ),
+ );
+ } else {
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled();
+ }
+ },
+ );
+
+ it.each`
+ extensions | thrownError
+ ${''} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${undefined} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{}} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ foo: 'bar' }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: '' }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: undefined }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: [] }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ ${{ definition: {} }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ ${{ definition: { foo: 'bar' } }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ `(
+ 'Should throw $thrownError when extension is "$extensions"',
+ ({ extensions, thrownError }) => {
+ seInstance = new Instance();
+ const useExtension = () => {
+ seInstance.use(extensions);
+ };
+ expect(useExtension).toThrowError(thrownError);
+ },
+ );
+
+ describe('global extensions registry', () => {
+ let extensionStore;
+
+ beforeEach(() => {
+ extensionStore = new Map();
+ seInstance = new Instance({}, extensionStore);
+ });
+
+ it('stores _instances_ of the used extensions in a global registry', () => {
+ const extension = seInstance.use({ definition: MyClassExtension });
+
+ expect(extensionStore.size).toBe(1);
+ expect(extensionStore.entries().next().value).toEqual(['MyClassExtension', extension]);
+ });
+
+ it('does not duplicate entries in the registry', () => {
+ jest.spyOn(extensionStore, 'set');
+
+ const extension1 = seInstance.use({ definition: MyClassExtension });
+ seInstance.use({ definition: MyClassExtension });
+
+ expect(extensionStore.set).toHaveBeenCalledTimes(1);
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ });
+
+ it.each`
+ desc | currentSetupOptions | newSetupOptions | expectedCallTimes
+ ${'updates'} | ${undefined} | ${defSetupOptions} | ${2}
+ ${'updates'} | ${defSetupOptions} | ${undefined} | ${2}
+ ${'updates'} | ${{ foo: 'bar' }} | ${{ foo: 'new' }} | ${2}
+ ${'does not update'} | ${undefined} | ${undefined} | ${1}
+ ${'does not update'} | ${{}} | ${{}} | ${1}
+ ${'does not update'} | ${defSetupOptions} | ${defSetupOptions} | ${1}
+ `(
+ '$desc the extensions entry when setupOptions "$currentSetupOptions" get changed to "$newSetupOptions"',
+ ({ currentSetupOptions, newSetupOptions, expectedCallTimes }) => {
+ jest.spyOn(extensionStore, 'set');
+
+ const extension1 = seInstance.use({
+ definition: MyClassExtension,
+ setupOptions: currentSetupOptions,
+ });
+ const extension2 = seInstance.use({
+ definition: MyClassExtension,
+ setupOptions: newSetupOptions,
+ });
+
+ expect(extensionStore.size).toBe(1);
+ expect(extensionStore.set).toHaveBeenCalledTimes(expectedCallTimes);
+ if (expectedCallTimes > 1) {
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension2);
+ } else {
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ }
+ },
+ );
+ });
+ });
+
+ describe('unuse', () => {
+ it.each`
+ unuseExtension | thrownError
+ ${undefined} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ ${''} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ ${{}} | ${sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { name: '' })}
+ ${[]} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ `(
+ `Should throw "${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}" when extension is "$unuseExtension"`,
+ ({ unuseExtension, thrownError }) => {
+ seInstance = new Instance();
+ const unuse = () => {
+ seInstance.unuse(unuseExtension);
+ };
+ expect(unuse).toThrowError(thrownError);
+ },
+ );
+
+ it.each`
+ initExtensions | unuseExtensionIndex | remainingAPI
+ ${{ definition: MyClassExtension }} | ${0} | ${[]}
+ ${{ definition: MyFnExtension }} | ${0} | ${[]}
+ ${{ definition: MyConstExt }} | ${0} | ${[]}
+ ${fullExtensionsArray} | ${0} | ${['fnExtMethod', 'constExtMethod']}
+ ${fullExtensionsArray} | ${1} | ${['shared', 'classExtMethod', 'constExtMethod']}
+ ${fullExtensionsArray} | ${2} | ${['shared', 'classExtMethod', 'fnExtMethod']}
+ `(
+ 'un-registers properties introduced by single extension $unuseExtension',
+ ({ initExtensions, unuseExtensionIndex, remainingAPI }) => {
+ seInstance = new Instance();
+ const extensions = seInstance.use(initExtensions);
+
+ if (Array.isArray(initExtensions)) {
+ seInstance.unuse(extensions[unuseExtensionIndex]);
+ } else {
+ seInstance.unuse(extensions);
+ }
+ expect(seInstance.extensionsAPI).toEqual(remainingAPI);
+ },
+ );
+
+ it.each`
+ unuseExtensionIndex | remainingAPI
+ ${[0, 1]} | ${['constExtMethod']}
+ ${[0, 2]} | ${['fnExtMethod']}
+ ${[1, 2]} | ${['shared', 'classExtMethod']}
+ `(
+ 'un-registers properties introduced by multiple extensions $unuseExtension',
+ ({ unuseExtensionIndex, remainingAPI }) => {
+ seInstance = new Instance();
+ const extensions = seInstance.use(fullExtensionsArray);
+ const extensionsToUnuse = extensions.filter((ext, index) =>
+ unuseExtensionIndex.includes(index),
+ );
+
+ seInstance.unuse(extensionsToUnuse);
+ expect(seInstance.extensionsAPI).toEqual(remainingAPI);
+ },
+ );
+
+ it('it does not remove entry from the global registry to keep for potential future re-use', () => {
+ const extensionStore = new Map();
+ seInstance = new Instance({}, extensionStore);
+ const extensions = seInstance.use(fullExtensionsArray);
+ const verifyExpectations = () => {
+ const entries = extensionStore.entries();
+ const mockExtensions = ['MyClassExtension', 'MyFnExtension', 'MyConstExt'];
+ expect(extensionStore.size).toBe(mockExtensions.length);
+ mockExtensions.forEach((ext, index) => {
+ expect(entries.next().value).toEqual([ext, extensions[index]]);
+ });
+ };
+
+ verifyExpectations();
+ seInstance.unuse(extensions);
+ verifyExpectations();
+ });
+ });
+
+ describe('updateModelLanguage', () => {
+ let instanceModel;
+
+ beforeEach(() => {
+ instanceModel = monacoEditor.createModel('');
+ seInstance = new Instance({
+ getModel: () => instanceModel,
+ });
+ });
+
+ it.each`
+ path | expectedLanguage
+ ${'foo.js'} | ${'javascript'}
+ ${'foo.md'} | ${'markdown'}
+ ${'foo.rb'} | ${'ruby'}
+ ${''} | ${'plaintext'}
+ ${undefined} | ${'plaintext'}
+ ${'test.nonexistingext'} | ${'plaintext'}
+ `(
+ 'changes language of an attached model to "$expectedLanguage" when filepath is "$path"',
+ ({ path, expectedLanguage }) => {
+ seInstance.updateModelLanguage(path);
+ expect(instanceModel.getLanguageIdentifier().language).toBe(expectedLanguage);
+ },
+ );
+ });
+
+ describe('extensions life-cycle callbacks', () => {
+ const onSetup = jest.fn().mockImplementation(() => {});
+ const onUse = jest.fn().mockImplementation(() => {});
+ const onBeforeUnuse = jest.fn().mockImplementation(() => {});
+ const onUnuse = jest.fn().mockImplementation(() => {});
+ const MyFullExtWithCallbacks = () => {
+ return {
+ onSetup,
+ onUse,
+ onBeforeUnuse,
+ onUnuse,
+ };
+ };
+
+ it('passes correct arguments to callback fns when using an extension', () => {
+ seInstance = new Instance();
+ seInstance.use({
+ definition: MyFullExtWithCallbacks,
+ setupOptions: defSetupOptions,
+ });
+ expect(onSetup).toHaveBeenCalledWith(defSetupOptions, seInstance);
+ expect(onUse).toHaveBeenCalledWith(seInstance);
+ });
+
+ it('passes correct arguments to callback fns when un-using an extension', () => {
+ seInstance = new Instance();
+ const extension = seInstance.use({
+ definition: MyFullExtWithCallbacks,
+ setupOptions: defSetupOptions,
+ });
+ seInstance.unuse(extension);
+ expect(onBeforeUnuse).toHaveBeenCalledWith(seInstance);
+ expect(onUnuse).toHaveBeenCalledWith(seInstance);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js
new file mode 100644
index 00000000000..97d2b0b21d0
--- /dev/null
+++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js
@@ -0,0 +1,449 @@
+import { Document } from 'yaml';
+import SourceEditor from '~/editor/source_editor';
+import { YamlEditorExtension } from '~/editor/extensions/source_editor_yaml_ext';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
+
+const getEditorInstance = (editorInstanceOptions = {}) => {
+ setFixtures('<div id="editor"></div>');
+ return new SourceEditor().createInstance({
+ el: document.getElementById('editor'),
+ blobPath: '.gitlab-ci.yml',
+ language: 'yaml',
+ ...editorInstanceOptions,
+ });
+};
+
+const getEditorInstanceWithExtension = (extensionOptions = {}, editorInstanceOptions = {}) => {
+ setFixtures('<div id="editor"></div>');
+ const instance = getEditorInstance(editorInstanceOptions);
+ instance.use(new YamlEditorExtension({ instance, ...extensionOptions }));
+
+ // Remove the below once
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/325992 is resolved
+ if (editorInstanceOptions.value && !extensionOptions.model) {
+ instance.setValue(editorInstanceOptions.value);
+ }
+
+ return instance;
+};
+
+describe('YamlCreatorExtension', () => {
+ describe('constructor', () => {
+ it('saves constructor options', () => {
+ const instance = getEditorInstanceWithExtension({
+ highlightPath: 'foo',
+ enableComments: true,
+ });
+ expect(instance).toEqual(
+ expect.objectContaining({
+ options: expect.objectContaining({
+ highlightPath: 'foo',
+ enableComments: true,
+ }),
+ }),
+ );
+ });
+
+ it('dumps values loaded with the model constructor options', () => {
+ const model = { foo: 'bar' };
+ const expected = 'foo: bar\n';
+ const instance = getEditorInstanceWithExtension({ model });
+ expect(instance.getDoc().get('foo')).toBeDefined();
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('registers the onUpdate() function', () => {
+ const instance = getEditorInstance();
+ const onDidChangeModelContent = jest.spyOn(instance, 'onDidChangeModelContent');
+ instance.use(new YamlEditorExtension({ instance }));
+ expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
+ });
+
+ it("If not provided with a load constructor option, it will parse the editor's value", () => {
+ const editorValue = 'foo: bar';
+ const instance = getEditorInstanceWithExtension({}, { value: editorValue });
+ expect(instance.getDoc().get('foo')).toBeDefined();
+ });
+
+ it("Prefers values loaded with the load constructor option over the editor's existing value", () => {
+ const editorValue = 'oldValue: this should be overriden';
+ const model = { thisShould: 'be the actual value' };
+ const expected = 'thisShould: be the actual value\n';
+ const instance = getEditorInstanceWithExtension({ model }, { value: editorValue });
+ expect(instance.getDoc().get('oldValue')).toBeUndefined();
+ expect(instance.getValue()).toEqual(expected);
+ });
+ });
+
+ describe('initFromModel', () => {
+ const model = { foo: 'bar', 1: 2, abc: ['def'] };
+ const doc = new Document(model);
+
+ it('should call transformComments if enableComments is true', () => {
+ const instance = getEditorInstanceWithExtension({ enableComments: true });
+ const transformComments = jest.spyOn(YamlEditorExtension, 'transformComments');
+ YamlEditorExtension.initFromModel(instance, model);
+ expect(transformComments).toHaveBeenCalled();
+ });
+
+ it('should not call transformComments if enableComments is false', () => {
+ const instance = getEditorInstanceWithExtension({ enableComments: false });
+ const transformComments = jest.spyOn(YamlEditorExtension, 'transformComments');
+ YamlEditorExtension.initFromModel(instance, model);
+ expect(transformComments).not.toHaveBeenCalled();
+ });
+
+ it('should call setValue with the stringified model', () => {
+ const instance = getEditorInstanceWithExtension();
+ const setValue = jest.spyOn(instance, 'setValue');
+ YamlEditorExtension.initFromModel(instance, model);
+ expect(setValue).toHaveBeenCalledWith(doc.toString());
+ });
+ });
+
+ describe('wrapCommentString', () => {
+ const longString =
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
+
+ it('should add spaces before each line', () => {
+ const result = YamlEditorExtension.wrapCommentString(longString);
+ const lines = result.split('\n');
+ expect(lines.every((ln) => ln.startsWith(' '))).toBe(true);
+ });
+
+ it('should break long comments into lines of max. 79 chars', () => {
+ // 79 = 80 char width minus 1 char for the '#' at the start of each line
+ const result = YamlEditorExtension.wrapCommentString(longString);
+ const lines = result.split('\n');
+ expect(lines.every((ln) => ln.length <= 79)).toBe(true);
+ });
+
+ it('should decrease the line width if passed a level by 2 chars per level', () => {
+ for (let i = 0; i <= 5; i += 1) {
+ const result = YamlEditorExtension.wrapCommentString(longString, i);
+ const lines = result.split('\n');
+ const decreaseLineWidthBy = i * 2;
+ const maxLineWith = 79 - decreaseLineWidthBy;
+ const isValidLine = (ln) => {
+ if (ln.length <= maxLineWith) return true;
+ // The line may exceed the max line width in case the word is the
+ // only one in the line and thus cannot be broken further
+ return ln.split(' ').length <= 1;
+ };
+ expect(lines.every(isValidLine)).toBe(true);
+ }
+ });
+
+ it('return null if passed an invalid string value', () => {
+ expect(YamlEditorExtension.wrapCommentString(null)).toBe(null);
+ expect(YamlEditorExtension.wrapCommentString()).toBe(null);
+ });
+
+ it('throw an error if passed an invalid level value', () => {
+ expect(() => YamlEditorExtension.wrapCommentString('abc', -5)).toThrow(
+ 'Invalid value "-5" for variable `level`',
+ );
+ expect(() => YamlEditorExtension.wrapCommentString('abc', 'invalid')).toThrow(
+ 'Invalid value "invalid" for variable `level`',
+ );
+ });
+ });
+
+ describe('transformComments', () => {
+ const getInstanceWithModel = (model) => {
+ return getEditorInstanceWithExtension({
+ model,
+ enableComments: true,
+ });
+ };
+
+ it('converts comments inside an array', () => {
+ const model = ['# test comment', 'def', '# foo', 999];
+ const expected = `# test comment\n- def\n# foo\n- 999\n`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('converts generic comments inside an object and places them at the top', () => {
+ const model = { foo: 'bar', 1: 2, '#': 'test comment' };
+ const expected = `# test comment\n"1": 2\nfoo: bar\n`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('adds specific comments before the mentioned entry of an object', () => {
+ const model = { foo: 'bar', 1: 2, '#|foo': 'foo comment' };
+ const expected = `"1": 2\n# foo comment\nfoo: bar\n`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('limits long comments to 80 char width, including indentation', () => {
+ const model = {
+ '#|foo':
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.',
+ foo: {
+ nested1: {
+ nested2: {
+ nested3: {
+ '#|bar':
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.',
+ bar: 'baz',
+ },
+ },
+ },
+ },
+ };
+ const expected = `# Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
+# eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
+# voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+foo:
+ nested1:
+ nested2:
+ nested3:
+ # Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
+ # nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
+ # sed diam voluptua. At vero eos et accusam et justo duo dolores et ea
+ # rebum.
+ bar: baz
+`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+ });
+
+ describe('getDoc', () => {
+ it('returns a yaml `Document` Type', () => {
+ const instance = getEditorInstanceWithExtension();
+ expect(instance.getDoc()).toBeInstanceOf(Document);
+ });
+ });
+
+ describe('setDoc', () => {
+ const model = { foo: 'bar', 1: 2, abc: ['def'] };
+ const doc = new Document(model);
+
+ it('should call transformComments if enableComments is true', () => {
+ const spy = jest.spyOn(YamlEditorExtension, 'transformComments');
+ const instance = getEditorInstanceWithExtension({ enableComments: true });
+ instance.setDoc(doc);
+ expect(spy).toHaveBeenCalledWith(doc);
+ });
+
+ it('should not call transformComments if enableComments is false', () => {
+ const spy = jest.spyOn(YamlEditorExtension, 'transformComments');
+ const instance = getEditorInstanceWithExtension({ enableComments: false });
+ instance.setDoc(doc);
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it("should call setValue with the stringified doc if the editor's value is empty", () => {
+ const instance = getEditorInstanceWithExtension();
+ const setValue = jest.spyOn(instance, 'setValue');
+ const updateValue = jest.spyOn(instance, 'updateValue');
+ instance.setDoc(doc);
+ expect(setValue).toHaveBeenCalledWith(doc.toString());
+ expect(updateValue).not.toHaveBeenCalled();
+ });
+
+ it("should call updateValue with the stringified doc if the editor's value is not empty", () => {
+ const instance = getEditorInstanceWithExtension({}, { value: 'asjkdhkasjdh' });
+ const setValue = jest.spyOn(instance, 'setValue');
+ const updateValue = jest.spyOn(instance, 'updateValue');
+ instance.setDoc(doc);
+ expect(setValue).not.toHaveBeenCalled();
+ expect(updateValue).toHaveBeenCalledWith(doc.toString());
+ });
+
+ it('should trigger the onUpdate method', () => {
+ const instance = getEditorInstanceWithExtension();
+ const onUpdate = jest.spyOn(instance, 'onUpdate');
+ instance.setDoc(doc);
+ expect(onUpdate).toHaveBeenCalled();
+ });
+ });
+
+ describe('getDataModel', () => {
+ it('returns the model as JS', () => {
+ const value = 'abc: def\nfoo:\n - bar\n - baz\n';
+ const expected = { abc: 'def', foo: ['bar', 'baz'] };
+ const instance = getEditorInstanceWithExtension({}, { value });
+ expect(instance.getDataModel()).toEqual(expected);
+ });
+ });
+
+ describe('setDataModel', () => {
+ it('sets the value to a YAML-representation of the Doc', () => {
+ const model = {
+ abc: ['def'],
+ '#|foo': 'foo comment',
+ foo: {
+ '#|abc': 'abc comment',
+ abc: [{ def: 'ghl', lorem: 'ipsum' }, '# array comment', null],
+ bar: 'baz',
+ },
+ };
+ const expected =
+ 'abc:\n' +
+ ' - def\n' +
+ '# foo comment\n' +
+ 'foo:\n' +
+ ' # abc comment\n' +
+ ' abc:\n' +
+ ' - def: ghl\n' +
+ ' lorem: ipsum\n' +
+ ' # array comment\n' +
+ ' - null\n' +
+ ' bar: baz\n';
+
+ const instance = getEditorInstanceWithExtension({ enableComments: true });
+ const setValue = jest.spyOn(instance, 'setValue');
+
+ instance.setDataModel(model);
+
+ expect(setValue).toHaveBeenCalledWith(expected);
+ });
+
+ it('causes the editor value to be updated', () => {
+ const initialModel = { foo: 'this should be overriden' };
+ const initialValue = 'foo: this should be overriden\n';
+ const newValue = { thisShould: 'be the actual value' };
+ const expected = 'thisShould: be the actual value\n';
+ const instance = getEditorInstanceWithExtension({ model: initialModel });
+ expect(instance.getValue()).toEqual(initialValue);
+ instance.setDataModel(newValue);
+ expect(instance.getValue()).toEqual(expected);
+ });
+ });
+
+ describe('onUpdate', () => {
+ it('calls highlight', () => {
+ const highlightPath = 'foo';
+ const instance = getEditorInstanceWithExtension({ highlightPath });
+ instance.highlight = jest.fn();
+ instance.onUpdate();
+ expect(instance.highlight).toHaveBeenCalledWith(highlightPath);
+ });
+ });
+
+ describe('updateValue', () => {
+ it("causes the editor's value to be updated", () => {
+ const oldValue = 'foobar';
+ const newValue = 'bazboo';
+ const instance = getEditorInstanceWithExtension({}, { value: oldValue });
+ instance.updateValue(newValue);
+ expect(instance.getValue()).toEqual(newValue);
+ });
+ });
+
+ describe('highlight', () => {
+ const highlightPathOnSetup = 'abc';
+ const value = `foo:
+ bar:
+ - baz
+ - boo
+ abc: def
+`;
+ let instance;
+ let highlightLinesSpy;
+ let removeHighlightsSpy;
+
+ beforeEach(() => {
+ instance = getEditorInstanceWithExtension({ highlightPath: highlightPathOnSetup }, { value });
+ highlightLinesSpy = jest.spyOn(SourceEditorExtension, 'highlightLines');
+ removeHighlightsSpy = jest.spyOn(SourceEditorExtension, 'removeHighlights');
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('saves the highlighted path in highlightPath', () => {
+ const path = 'foo.bar';
+ instance.highlight(path);
+ expect(instance.options.highlightPath).toEqual(path);
+ });
+
+ it('calls highlightLines with a number of lines', () => {
+ const path = 'foo.bar';
+ instance.highlight(path);
+ expect(highlightLinesSpy).toHaveBeenCalledWith(instance, [2, 4]);
+ });
+
+ it('calls removeHighlights if path is null', () => {
+ instance.highlight(null);
+ expect(removeHighlightsSpy).toHaveBeenCalledWith(instance);
+ expect(highlightLinesSpy).not.toHaveBeenCalled();
+ expect(instance.options.highlightPath).toBeNull();
+ });
+
+ it('throws an error if path is invalid and does not change the highlighted path', () => {
+ expect(() => instance.highlight('invalidPath[0]')).toThrow(
+ 'The node invalidPath[0] could not be found inside the document.',
+ );
+ expect(instance.options.highlightPath).toEqual(highlightPathOnSetup);
+ expect(highlightLinesSpy).not.toHaveBeenCalled();
+ expect(removeHighlightsSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('locate', () => {
+ const options = {
+ enableComments: true,
+ model: {
+ abc: ['def'],
+ '#|foo': 'foo comment',
+ foo: {
+ '#|abc': 'abc comment',
+ abc: [{ def: 'ghl', lorem: 'ipsum' }, '# array comment', null],
+ bar: 'baz',
+ },
+ },
+ };
+
+ const value =
+ /* 1 */ 'abc:\n' +
+ /* 2 */ ' - def\n' +
+ /* 3 */ '# foo comment\n' +
+ /* 4 */ 'foo:\n' +
+ /* 5 */ ' # abc comment\n' +
+ /* 6 */ ' abc:\n' +
+ /* 7 */ ' - def: ghl\n' +
+ /* 8 */ ' lorem: ipsum\n' +
+ /* 9 */ ' # array comment\n' +
+ /* 10 */ ' - null\n' +
+ /* 11 */ ' bar: baz\n';
+
+ it('asserts that the test setup is correct', () => {
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.getValue()).toEqual(value);
+ });
+
+ it('returns the expected line numbers for a path to an object inside the yaml', () => {
+ const path = 'foo.abc';
+ const expected = [6, 10];
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.locate(path)).toEqual(expected);
+ });
+
+ it('throws an error if a path cannot be found inside the yaml', () => {
+ const path = 'baz[8]';
+ const instance = getEditorInstanceWithExtension(options);
+ expect(() => instance.locate(path)).toThrow();
+ });
+
+ it('returns the expected line numbers for a path to an array entry inside the yaml', () => {
+ const path = 'foo.abc[0]';
+ const expected = [7, 8];
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.locate(path)).toEqual(expected);
+ });
+
+ it('returns the expected line numbers for a path that includes a comment inside the yaml', () => {
+ const path = 'foo';
+ const expected = [4, 11];
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.locate(path)).toEqual(expected);
+ });
+ });
+});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
new file mode 100644
index 00000000000..e56b6448b7d
--- /dev/null
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -0,0 +1,530 @@
+export const environmentsApp = {
+ environments: [
+ {
+ name: 'review',
+ size: 2,
+ latest: {
+ id: 42,
+ global_id: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: 'review',
+ name_without_type: 'goodbye',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/42',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/42',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ created_at: '2021-10-04T19:27:20.639Z',
+ updated_at: '2021-10-04T19:27:20.639Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ },
+ {
+ name: 'production',
+ size: 1,
+ latest: {
+ id: 8,
+ global_id: 'gid://gitlab/Environment/8',
+ name: 'production',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: null,
+ name_without_type: 'production',
+ last_deployment: {
+ id: 80,
+ iid: 24,
+ sha: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ ref: {
+ name: 'root-master-patch-18104',
+ ref_path: '/h5bp/html5-boilerplate/-/tree/root-master-patch-18104',
+ },
+ status: 'success',
+ created_at: '2021-10-08T19:53:54.543Z',
+ deployed_at: '2021-10-08T20:02:36.763Z',
+ tag: false,
+ 'last?': true,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ deployable: {
+ id: 911,
+ name: 'deploy-job',
+ started: '2021-10-08T19:54:00.658Z',
+ complete: true,
+ archived: false,
+ build_path: '/h5bp/html5-boilerplate/-/jobs/911',
+ retry_path: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ play_path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ playable: true,
+ scheduled: false,
+ created_at: '2021-10-08T19:53:54.482Z',
+ updated_at: '2021-10-08T20:02:36.730Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'manual play action',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/h5bp/html5-boilerplate/-/jobs/911',
+ illustration: {
+ image:
+ '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
+ size: 'svg-394',
+ title: 'This job requires a manual action',
+ content:
+ 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ method: 'post',
+ button_title: 'Trigger this manual action',
+ },
+ },
+ },
+ commit: {
+ id: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ short_id: '4ca03103',
+ created_at: '2021-10-08T19:27:01.000+00:00',
+ parent_ids: ['b385360b15bd61391a0efbd101788d4a80387270'],
+ title: 'Update .gitlab-ci.yml',
+ message: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2021-10-08T19:27:01.000+00:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2021-10-08T19:27:01.000+00:00',
+ trailers: {},
+ web_url:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ author_gravatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ commit_path:
+ '/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ },
+ manual_actions: [],
+ scheduled_actions: [],
+ playable_build: {
+ retry_path: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ play_path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ },
+ cluster: null,
+ },
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/8',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/8/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/8/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/8',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/production',
+ created_at: '2021-06-17T15:09:38.599Z',
+ updated_at: '2021-10-08T19:50:44.445Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=production',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=production',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ },
+ {
+ name: 'staging',
+ size: 1,
+ latest: {
+ id: 7,
+ global_id: 'gid://gitlab/Environment/7',
+ name: 'staging',
+ state: 'available',
+ external_url: null,
+ environment_type: null,
+ name_without_type: 'staging',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/7',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/7/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/7/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/7',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/staging',
+ created_at: '2021-06-17T15:09:38.570Z',
+ updated_at: '2021-06-17T15:09:38.570Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=staging',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=staging',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ },
+ ],
+ review_app: {
+ can_setup_review_app: true,
+ all_clusters_empty: true,
+ review_snippet:
+ '{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
+ },
+ available_count: 4,
+ stopped_count: 0,
+};
+
+export const resolvedEnvironmentsApp = {
+ availableCount: 4,
+ environments: [
+ {
+ name: 'review',
+ size: 2,
+ latest: {
+ id: 42,
+ globalId: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: 'review',
+ nameWithoutType: 'goodbye',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/42',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/42',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ createdAt: '2021-10-04T19:27:20.639Z',
+ updatedAt: '2021-10-04T19:27:20.639Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ },
+ __typename: 'NestedLocalEnvironment',
+ },
+ {
+ name: 'production',
+ size: 1,
+ latest: {
+ id: 8,
+ globalId: 'gid://gitlab/Environment/8',
+ name: 'production',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: null,
+ nameWithoutType: 'production',
+ lastDeployment: {
+ id: 80,
+ iid: 24,
+ sha: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ ref: {
+ name: 'root-master-patch-18104',
+ refPath: '/h5bp/html5-boilerplate/-/tree/root-master-patch-18104',
+ },
+ status: 'success',
+ createdAt: '2021-10-08T19:53:54.543Z',
+ deployedAt: '2021-10-08T20:02:36.763Z',
+ tag: false,
+ 'last?': true,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gdk.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ deployable: {
+ id: 911,
+ name: 'deploy-job',
+ started: '2021-10-08T19:54:00.658Z',
+ complete: true,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/911',
+ retryPath: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ playPath: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ playable: true,
+ scheduled: false,
+ createdAt: '2021-10-08T19:53:54.482Z',
+ updatedAt: '2021-10-08T20:02:36.730Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'manual play action',
+ group: 'success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/911',
+ illustration: {
+ image:
+ '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
+ size: 'svg-394',
+ title: 'This job requires a manual action',
+ content:
+ 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ method: 'post',
+ buttonTitle: 'Trigger this manual action',
+ },
+ },
+ },
+ commit: {
+ id: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ shortId: '4ca03103',
+ createdAt: '2021-10-08T19:27:01.000+00:00',
+ parentIds: ['b385360b15bd61391a0efbd101788d4a80387270'],
+ title: 'Update .gitlab-ci.yml',
+ message: 'Update .gitlab-ci.yml',
+ authorName: 'Administrator',
+ authorEmail: 'admin@example.com',
+ authoredDate: '2021-10-08T19:27:01.000+00:00',
+ committerName: 'Administrator',
+ committerEmail: 'admin@example.com',
+ committedDate: '2021-10-08T19:27:01.000+00:00',
+ trailers: {},
+ webUrl:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gdk.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ authorGravatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commitUrl:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ commitPath: '/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ },
+ manualActions: [],
+ scheduledActions: [],
+ playableBuild: {
+ retryPath: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ playPath: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ },
+ cluster: null,
+ },
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/8',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/8/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/8/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/8',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/production',
+ createdAt: '2021-06-17T15:09:38.599Z',
+ updatedAt: '2021-10-08T19:50:44.445Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=production',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=production',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ },
+ __typename: 'NestedLocalEnvironment',
+ },
+ {
+ name: 'staging',
+ size: 1,
+ latest: {
+ id: 7,
+ globalId: 'gid://gitlab/Environment/7',
+ name: 'staging',
+ state: 'available',
+ externalUrl: null,
+ environmentType: null,
+ nameWithoutType: 'staging',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/7',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/7/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/7/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/7',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/staging',
+ createdAt: '2021-06-17T15:09:38.570Z',
+ updatedAt: '2021-06-17T15:09:38.570Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=staging',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=staging',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ },
+ __typename: 'NestedLocalEnvironment',
+ },
+ ],
+ reviewApp: {
+ canSetupReviewApp: true,
+ allClustersEmpty: true,
+ reviewSnippet:
+ '{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
+ __typename: 'ReviewApp',
+ },
+ stoppedCount: 0,
+ __typename: 'LocalEnvironmentApp',
+};
+
+export const folder = {
+ environments: [
+ {
+ id: 42,
+ global_id: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: 'review',
+ name_without_type: 'goodbye',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/42',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/42',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ created_at: '2021-10-04T19:27:20.639Z',
+ updated_at: '2021-10-04T19:27:20.639Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ {
+ id: 41,
+ global_id: 'gid://gitlab/Environment/41',
+ name: 'review/hello',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: 'review',
+ name_without_type: 'hello',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/41',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/41/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/41/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/41',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ created_at: '2021-10-04T19:27:00.527Z',
+ updated_at: '2021-10-04T19:27:00.527Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fhello',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fhello',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ ],
+ available_count: 2,
+ stopped_count: 0,
+};
+
+export const resolvedFolder = {
+ availableCount: 2,
+ environments: [
+ {
+ id: 42,
+ globalId: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: 'review',
+ nameWithoutType: 'goodbye',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/42',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/42',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ createdAt: '2021-10-04T19:27:20.639Z',
+ updatedAt: '2021-10-04T19:27:20.639Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ __typename: 'LocalEnvironment',
+ },
+ {
+ id: 41,
+ globalId: 'gid://gitlab/Environment/41',
+ name: 'review/hello',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: 'review',
+ nameWithoutType: 'hello',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/41',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/41/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/41/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/41',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ createdAt: '2021-10-04T19:27:00.527Z',
+ updatedAt: '2021-10-04T19:27:00.527Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fhello',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fhello',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ __typename: 'LocalEnvironment',
+ },
+ ],
+ stoppedCount: 0,
+ __typename: 'LocalEnvironmentFolder',
+};
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
new file mode 100644
index 00000000000..4d2a0818996
--- /dev/null
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -0,0 +1,91 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { resolvers } from '~/environments/graphql/resolvers';
+import { TEST_HOST } from 'helpers/test_constants';
+import { environmentsApp, resolvedEnvironmentsApp, folder, resolvedFolder } from './mock_data';
+
+const ENDPOINT = `${TEST_HOST}/environments`;
+
+describe('~/frontend/environments/graphql/resolvers', () => {
+ let mockResolvers;
+ let mock;
+
+ beforeEach(() => {
+ mockResolvers = resolvers(ENDPOINT);
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+
+ describe('environmentApp', () => {
+ it('should fetch environments and map them to frontend data', async () => {
+ mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp);
+
+ const app = await mockResolvers.Query.environmentApp();
+ expect(app).toEqual(resolvedEnvironmentsApp);
+ });
+ });
+ describe('folder', () => {
+ it('should fetch the folder url passed to it', async () => {
+ mock.onGet(ENDPOINT, { params: { per_page: 3 } }).reply(200, folder);
+
+ const environmentFolder = await mockResolvers.Query.folder(null, {
+ environment: { folderPath: ENDPOINT },
+ });
+
+ expect(environmentFolder).toEqual(resolvedFolder);
+ });
+ });
+ describe('stopEnvironment', () => {
+ it('should post to the stop environment path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.stopEnvironment(null, { environment: { stopPath: ENDPOINT } });
+
+ expect(mock.history.post).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'post' }),
+ );
+ });
+ });
+ describe('rollbackEnvironment', () => {
+ it('should post to the retry environment path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.rollbackEnvironment(null, {
+ environment: { retryUrl: ENDPOINT },
+ });
+
+ expect(mock.history.post).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'post' }),
+ );
+ });
+ });
+ describe('deleteEnvironment', () => {
+ it('should DELETE to the delete environment path', async () => {
+ mock.onDelete(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.deleteEnvironment(null, {
+ environment: { deletePath: ENDPOINT },
+ });
+
+ expect(mock.history.delete).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'delete' }),
+ );
+ });
+ });
+ describe('cancelAutoStop', () => {
+ it('should post to the auto stop path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.cancelAutoStop(null, {
+ environment: { autoStopPath: ENDPOINT },
+ });
+
+ expect(mock.history.post).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'post' }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js
new file mode 100644
index 00000000000..5696e187a86
--- /dev/null
+++ b/spec/frontend/environments/new_environment_folder_spec.js
@@ -0,0 +1,74 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlCollapse, GlIcon } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
+import { s__ } from '~/locale';
+import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/new_environments_folder.vue', () => {
+ let wrapper;
+ let environmentFolderMock;
+ let nestedEnvironment;
+ let folderName;
+
+ const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') });
+
+ const createApolloProvider = () => {
+ const mockResolvers = { Query: { folder: environmentFolderMock } };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (propsData, apolloProvider) =>
+ mountExtended(EnvironmentsFolder, { apolloProvider, propsData });
+
+ beforeEach(() => {
+ environmentFolderMock = jest.fn();
+ [nestedEnvironment] = resolvedEnvironmentsApp.environments;
+ environmentFolderMock.mockReturnValue(resolvedFolder);
+ wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
+ folderName = wrapper.findByText(nestedEnvironment.name);
+ });
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('displays the name of the folder', () => {
+ expect(folderName.text()).toBe(nestedEnvironment.name);
+ });
+
+ describe('collapse', () => {
+ let icons;
+ let collapse;
+
+ beforeEach(() => {
+ collapse = wrapper.findComponent(GlCollapse);
+ icons = wrapper.findAllComponents(GlIcon);
+ });
+
+ it('is collapsed by default', () => {
+ const link = findLink();
+
+ expect(collapse.attributes('visible')).toBeUndefined();
+ expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-right', 'folder-o']);
+ expect(folderName.classes('gl-font-weight-bold')).toBe(false);
+ expect(link.exists()).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ await folderName.trigger('click');
+
+ const link = findLink();
+
+ expect(collapse.attributes('visible')).toBe('true');
+ expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-down', 'folder-open']);
+ expect(folderName.classes('gl-font-weight-bold')).toBe(true);
+ expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js
new file mode 100644
index 00000000000..0ad8e8f442c
--- /dev/null
+++ b/spec/frontend/environments/new_environments_app_spec.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
+import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
+import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/new_environments_app.vue', () => {
+ let wrapper;
+ let environmentAppMock;
+ let environmentFolderMock;
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: { environmentApp: environmentAppMock, folder: environmentFolderMock },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider) => mount(EnvironmentsApp, { apolloProvider });
+
+ beforeEach(() => {
+ environmentAppMock = jest.fn();
+ environmentFolderMock = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('should show all the folders that are fetched', async () => {
+ environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
+ environmentFolderMock.mockReturnValue(resolvedFolder);
+ const apolloProvider = createApolloProvider();
+ wrapper = createWrapper(apolloProvider);
+
+ await waitForPromises();
+ await Vue.nextTick();
+
+ const text = wrapper.findAllComponents(EnvironmentsFolder).wrappers.map((w) => w.text());
+
+ expect(text).toContainEqual(expect.stringMatching('review'));
+ expect(text).not.toContainEqual(expect.stringMatching('production'));
+ });
+});
diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js
index de060f5eb8c..923795ca3f3 100644
--- a/spec/frontend/experimentation/utils_spec.js
+++ b/spec/frontend/experimentation/utils_spec.js
@@ -1,4 +1,4 @@
-import { assignGitlabExperiment } from 'helpers/experimentation_helper';
+import { stubExperiments } from 'helpers/experimentation_helper';
import {
DEFAULT_VARIANT,
CANDIDATE_VARIANT,
@@ -7,15 +7,45 @@ import {
import * as experimentUtils from '~/experimentation/utils';
describe('experiment Utilities', () => {
- const TEST_KEY = 'abc';
+ const ABC_KEY = 'abc';
+ const DEF_KEY = 'def';
+
+ let origGon;
+ let origGl;
+
+ beforeEach(() => {
+ origGon = window.gon;
+ origGl = window.gl;
+ window.gon.experiment = {};
+ window.gl.experiments = {};
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ window.gl = origGl;
+ });
describe('getExperimentData', () => {
+ const ABC_DATA = '_abc_data_';
+ const ABC_DATA2 = '_updated_abc_data_';
+ const DEF_DATA = '_def_data_';
+
describe.each`
- gon | input | output
- ${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${{ variant: '_data_' }}
- ${[]} | ${[TEST_KEY]} | ${undefined}
- `('with input=$input and gon=$gon', ({ gon, input, output }) => {
- assignGitlabExperiment(...gon);
+ gonData | glData | input | output
+ ${[ABC_KEY, ABC_DATA]} | ${[]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[]} | ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[DEF_KEY]} | ${{ experiment: DEF_KEY, variant: DEF_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY, ABC_DATA2]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA2 }}
+ ${[]} | ${[]} | ${[ABC_KEY]} | ${undefined}
+ `('with input=$input, gon=$gonData, & gl=$glData', ({ gonData, glData, input, output }) => {
+ beforeEach(() => {
+ const [gonKey, gonVariant] = gonData;
+ const [glKey, glVariant] = glData;
+
+ if (gonKey) window.gon.experiment[gonKey] = { experiment: gonKey, variant: gonVariant };
+ if (glKey) window.gl.experiments[glKey] = { experiment: glKey, variant: glVariant };
+ });
it(`returns ${output}`, () => {
expect(experimentUtils.getExperimentData(...input)).toEqual(output);
@@ -25,106 +55,129 @@ describe('experiment Utilities', () => {
describe('getAllExperimentContexts', () => {
const schema = TRACKING_CONTEXT_SCHEMA;
- let origGon;
-
- beforeEach(() => {
- origGon = window.gon;
- });
-
- afterEach(() => {
- window.gon = origGon;
- });
it('collects all of the experiment contexts into a single array', () => {
- const experiments = [
- { experiment: 'abc', variant: 'candidate' },
- { experiment: 'def', variant: 'control' },
- { experiment: 'ghi', variant: 'blue' },
- ];
- window.gon = {
- experiment: experiments.reduce((collector, { experiment, variant }) => {
- return { ...collector, [experiment]: { experiment, variant } };
- }, {}),
- };
+ const experiments = { [ABC_KEY]: 'candidate', [DEF_KEY]: 'control', ghi: 'blue' };
+
+ stubExperiments(experiments);
expect(experimentUtils.getAllExperimentContexts()).toEqual(
- experiments.map((data) => ({ schema, data })),
+ Object.entries(experiments).map(([experiment, variant]) => ({
+ schema,
+ data: { experiment, variant },
+ })),
);
});
it('returns an empty array if there are no experiments', () => {
- window.gon.experiment = {};
-
expect(experimentUtils.getAllExperimentContexts()).toEqual([]);
});
- it('includes all additional experiment data', () => {
- const experiment = 'experimentWithCustomData';
- const data = { experiment, variant: 'control', color: 'blue', style: 'rounded' };
- window.gon.experiment[experiment] = data;
+ it('only collects the data properties which are supported by the schema', () => {
+ origGl = window.gl;
+ window.gl.experiments = {
+ my_experiment: { experiment: 'my_experiment', variant: 'control', excluded: false },
+ };
+
+ expect(experimentUtils.getAllExperimentContexts()).toEqual([
+ { schema, data: { experiment: 'my_experiment', variant: 'control' } },
+ ]);
- expect(experimentUtils.getAllExperimentContexts()).toContainEqual({ schema, data });
+ window.gl = origGl;
});
});
describe('isExperimentVariant', () => {
describe.each`
- gon | input | output
- ${[TEST_KEY, DEFAULT_VARIANT]} | ${[TEST_KEY, DEFAULT_VARIANT]} | ${true}
- ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_variant_name']} | ${true}
- ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_bogus_name']} | ${false}
- ${[TEST_KEY, '_variant_name']} | ${['boguskey', '_variant_name']} | ${false}
- ${[]} | ${[TEST_KEY, '_variant_name']} | ${false}
- `('with input=$input and gon=$gon', ({ gon, input, output }) => {
- assignGitlabExperiment(...gon);
-
- it(`returns ${output}`, () => {
- expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
- });
- });
+ experiment | variant | input | output
+ ${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true}
+ ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true}
+ ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false}
+ ${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false}
+ ${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false}
+ `(
+ 'with input=$input, experiment=$experiment, variant=$variant',
+ ({ experiment, variant, input, output }) => {
+ it(`returns ${output}`, () => {
+ if (experiment) stubExperiments({ [experiment]: variant });
+
+ expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
+ });
+ },
+ );
});
describe('experiment', () => {
+ const experiment = 'marley';
+ const useSpy = jest.fn();
const controlSpy = jest.fn();
+ const trySpy = jest.fn();
const candidateSpy = jest.fn();
const getUpStandUpSpy = jest.fn();
const variants = {
- use: controlSpy,
- try: candidateSpy,
+ use: useSpy,
+ try: trySpy,
get_up_stand_up: getUpStandUpSpy,
};
describe('when there is no experiment data', () => {
- it('calls control variant', () => {
- experimentUtils.experiment('marley', variants);
- expect(controlSpy).toHaveBeenCalled();
+ it('calls the use variant', () => {
+ experimentUtils.experiment(experiment, variants);
+ expect(useSpy).toHaveBeenCalled();
+ });
+
+ describe("when 'control' is provided instead of 'use'", () => {
+ it('calls the control variant', () => {
+ experimentUtils.experiment(experiment, { control: controlSpy });
+ expect(controlSpy).toHaveBeenCalled();
+ });
});
});
describe('when experiment variant is "control"', () => {
- assignGitlabExperiment('marley', DEFAULT_VARIANT);
+ beforeEach(() => {
+ stubExperiments({ [experiment]: DEFAULT_VARIANT });
+ });
- it('calls the control variant', () => {
- experimentUtils.experiment('marley', variants);
- expect(controlSpy).toHaveBeenCalled();
+ it('calls the use variant', () => {
+ experimentUtils.experiment(experiment, variants);
+ expect(useSpy).toHaveBeenCalled();
+ });
+
+ describe("when 'control' is provided instead of 'use'", () => {
+ it('calls the control variant', () => {
+ experimentUtils.experiment(experiment, { control: controlSpy });
+ expect(controlSpy).toHaveBeenCalled();
+ });
});
});
describe('when experiment variant is "candidate"', () => {
- assignGitlabExperiment('marley', CANDIDATE_VARIANT);
+ beforeEach(() => {
+ stubExperiments({ [experiment]: CANDIDATE_VARIANT });
+ });
- it('calls the candidate variant', () => {
- experimentUtils.experiment('marley', variants);
- expect(candidateSpy).toHaveBeenCalled();
+ it('calls the try variant', () => {
+ experimentUtils.experiment(experiment, variants);
+ expect(trySpy).toHaveBeenCalled();
+ });
+
+ describe("when 'candidate' is provided instead of 'try'", () => {
+ it('calls the candidate variant', () => {
+ experimentUtils.experiment(experiment, { candidate: candidateSpy });
+ expect(candidateSpy).toHaveBeenCalled();
+ });
});
});
describe('when experiment variant is "get_up_stand_up"', () => {
- assignGitlabExperiment('marley', 'get_up_stand_up');
+ beforeEach(() => {
+ stubExperiments({ [experiment]: 'get_up_stand_up' });
+ });
it('calls the get-up-stand-up variant', () => {
- experimentUtils.experiment('marley', variants);
+ experimentUtils.experiment(experiment, variants);
expect(getUpStandUpSpy).toHaveBeenCalled();
});
});
@@ -132,14 +185,17 @@ describe('experiment Utilities', () => {
describe('getExperimentVariant', () => {
it.each`
- gon | input | output
- ${{ experiment: { [TEST_KEY]: { variant: DEFAULT_VARIANT } } }} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
- ${{ experiment: { [TEST_KEY]: { variant: CANDIDATE_VARIANT } } }} | ${[TEST_KEY]} | ${CANDIDATE_VARIANT}
- ${{}} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
- `('with input=$input and gon=$gon, returns $output', ({ gon, input, output }) => {
- window.gon = gon;
-
- expect(experimentUtils.getExperimentVariant(...input)).toEqual(output);
- });
+ experiment | variant | input | output
+ ${ABC_KEY} | ${DEFAULT_VARIANT} | ${ABC_KEY} | ${DEFAULT_VARIANT}
+ ${ABC_KEY} | ${CANDIDATE_VARIANT} | ${ABC_KEY} | ${CANDIDATE_VARIANT}
+ ${undefined} | ${undefined} | ${ABC_KEY} | ${DEFAULT_VARIANT}
+ `(
+ 'with input=$input, experiment=$experiment, & variant=$variant; returns $output',
+ ({ experiment, variant, input, output }) => {
+ stubExperiments({ [experiment]: variant });
+
+ expect(experimentUtils.getExperimentVariant(input)).toEqual(output);
+ },
+ );
});
});
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index 27ec6a7280f..f244da228b3 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -1,5 +1,6 @@
import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Component from '~/feature_flags/components/configure_feature_flags_modal.vue';
describe('Configure Feature Flags Modal', () => {
@@ -20,7 +21,7 @@ describe('Configure Feature Flags Modal', () => {
};
let wrapper;
- const factory = (props = {}, { mountFn = shallowMount, ...options } = {}) => {
+ const factory = (props = {}, { mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(Component, {
provide,
stubs: { GlSprintf },
@@ -140,11 +141,13 @@ describe('Configure Feature Flags Modal', () => {
describe('has rotate error', () => {
afterEach(() => wrapper.destroy());
- beforeEach(factory.bind(null, { hasRotateError: false }));
+ beforeEach(() => {
+ factory({ hasRotateError: true });
+ });
it('should display an error', async () => {
- expect(wrapper.find('.text-danger')).toExist();
- expect(wrapper.find('[name="warning"]')).toExist();
+ expect(wrapper.findByTestId('rotate-error').exists()).toBe(true);
+ expect(wrapper.find('[name="warning"]').exists()).toBe(true);
});
});
diff --git a/spec/frontend/filterable_list_spec.js b/spec/frontend/filterable_list_spec.js
index 556cf6f8137..3fd5d198e3a 100644
--- a/spec/frontend/filterable_list_spec.js
+++ b/spec/frontend/filterable_list_spec.js
@@ -1,5 +1,4 @@
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture, setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture } from 'helpers/fixtures';
import FilterableList from '~/filterable_list';
describe('FilterableList', () => {
@@ -15,8 +14,6 @@ describe('FilterableList', () => {
</div>
<div class="js-projects-list-holder"></div>
`);
- // eslint-disable-next-line import/no-deprecated
- getJSONFixture('static/projects.json');
form = document.querySelector('form#project-filter-form');
filter = document.querySelector('.js-projects-list-filter');
holder = document.querySelector('.js-projects-list-holder');
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 45f73260887..8fd6a5531db 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -2,63 +2,75 @@
# spec/frontend/fixtures/api_markdown.rb and
# spec/frontend/content_editor/extensions/markdown_processing_spec.js
---
+- name: attachment_image
+ context: group
+ markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
+- name: attachment_image
+ context: project
+ markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
+- name: attachment_image
+ context: project_wiki
+ markdown: '![test-file](test-file.png)'
+- name: attachment_link
+ context: group
+ markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
+- name: attachment_link
+ context: project
+ markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
+- name: attachment_link
+ context: project_wiki
+ markdown: '[test-file](test-file.zip)'
+- name: audio
+ markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
+- name: audio_and_video_in_lists
+ markdown: |-
+ * ![Sample Audio](https://gitlab.com/1.mp3)
+ * ![Sample Video](https://gitlab.com/2.mp4)
+
+ 1. ![Sample Video](https://gitlab.com/1.mp4)
+ 2. ![Sample Audio](https://gitlab.com/2.mp3)
+
+ * [x] ![Sample Audio](https://gitlab.com/1.mp3)
+ * [x] ![Sample Audio](https://gitlab.com/2.mp3)
+ * [x] ![Sample Video](https://gitlab.com/3.mp4)
+- name: blockquote
+ markdown: |-
+ > This is a blockquote
+ >
+ > This is another one
- name: bold
markdown: '**bold**'
-- name: emphasis
- markdown: '_emphasized text_'
-- name: inline_code
- markdown: '`code`'
-- name: inline_diff
+- name: bullet_list_style_1
markdown: |-
- * {-deleted-}
- * {+added+}
-- name: strike
- markdown: '~~del~~'
-- name: horizontal_rule
- markdown: '---'
-- name: html_marks
+ * list item 1
+ * list item 2
+ * embedded list item 3
+- name: bullet_list_style_2
markdown: |-
- * Content editor is ~~great~~<ins>amazing</ins>.
- * If the changes <abbr title="Looks good to merge">LGTM</abbr>, please <abbr title="Merge when pipeline succeeds">MWPS</abbr>.
- * The English song <q>Oh I do like to be beside the seaside</q> looks like this in Hebrew: <span dir="rtl">אה, אני אוהב להיות ליד חוף הים</span>. In the computer's memory, this is stored as <bdo dir="ltr">אה, אני אוהב להיות ליד חוף הים</bdo>.
- * <cite>The Scream</cite> by Edvard Munch. Painted in 1893.
- * <dfn>HTML</dfn> is the standard markup language for creating web pages.
- * Do not forget to buy <mark>milk</mark> today.
- * This is a paragraph and <small>smaller text goes here</small>.
- * The concert starts at <time datetime="20:00">20:00</time> and you'll be able to enjoy the band for at least <time datetime="PT2H30M">2h 30m</time>.
- * Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text (Windows).
- * WWF's goal is to: <q>Build a future where people live in harmony with nature.</q> We hope they succeed.
- * The error occured was: <samp>Keyboard not found. Press F1 to continue.</samp>
- * The area of a triangle is: 1/2 x <var>b</var> x <var>h</var>, where <var>b</var> is the base, and <var>h</var> is the vertical height.
- * <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
- * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
- * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
-- name: div
+ - list item 1
+ - list item 2
+ * embedded list item 3
+- name: bullet_list_style_3
markdown: |-
- <div>plain text</div>
- <div>
-
- just a plain ol' div, not much to _expect_!
-
- </div>
-- name: figure
+ + list item 1
+ + list item 2
+ - embedded list item 3
+- name: code_block
markdown: |-
- <figure>
-
- ![Elephant at sunset](elephant-sunset.jpg)
-
- <figcaption>An elephant at sunset</figcaption>
- </figure>
- <figure>
-
- ![A crocodile wearing crocs](croc-crocs.jpg)
-
- <figcaption>
-
- A crocodile wearing _crocs_!
-
- </figcaption>
- </figure>
+ ```javascript
+ console.log('hello world')
+ ```
+- name: color_chips
+ markdown: |-
+ - `#F00`
+ - `#F00A`
+ - `#FF0000`
+ - `#FF0000AA`
+ - `RGB(0,255,0)`
+ - `RGB(0%,100%,0%)`
+ - `RGBA(0,255,0,0.3)`
+ - `HSL(540,70%,50%)`
+ - `HSLA(540,70%,50%,0.3)`
- name: description_list
markdown: |-
<dl>
@@ -106,31 +118,57 @@
```
</details>
-- name: link
- markdown: '[GitLab](https://gitlab.com)'
-- name: attachment_link
- context: project_wiki
- markdown: '[test-file](test-file.zip)'
-- name: attachment_link
- context: project
- markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
-- name: attachment_link
- context: group
- markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
-- name: attachment_image
- context: project_wiki
- markdown: '![test-file](test-file.png)'
-- name: attachment_image
- context: project
- markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
-- name: attachment_image
- context: group
- markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
-- name: code_block
+- name: div
markdown: |-
- ```javascript
- console.log('hello world')
- ```
+ <div>plain text</div>
+ <div>
+
+ just a plain ol' div, not much to _expect_!
+
+ </div>
+- name: emoji
+ markdown: ':sparkles: :heart: :100:'
+- name: emphasis
+ markdown: '_emphasized text_'
+- name: figure
+ markdown: |-
+ <figure>
+
+ ![Elephant at sunset](elephant-sunset.jpg)
+
+ <figcaption>An elephant at sunset</figcaption>
+ </figure>
+ <figure>
+
+ ![A crocodile wearing crocs](croc-crocs.jpg)
+
+ <figcaption>
+
+ A crocodile wearing _crocs_!
+
+ </figcaption>
+ </figure>
+- name: frontmatter_json
+ markdown: |-
+ ;;;
+ {
+ "title": "Page title"
+ }
+ ;;;
+- name: frontmatter_toml
+ markdown: |-
+ +++
+ title = "Page title"
+ +++
+- name: frontmatter_yaml
+ markdown: |-
+ ---
+ title: Page title
+ ---
+- name: hard_break
+ markdown: |-
+ This is a line after a\
+ hard break
- name: headings
markdown: |-
# Heading 1
@@ -144,29 +182,44 @@
##### Heading 5
###### Heading 6
-- name: blockquote
- markdown: |-
- > This is a blockquote
- >
- > This is another one
-- name: thematic_break
- markdown: |-
- ---
-- name: bullet_list_style_1
+- name: horizontal_rule
+ markdown: '---'
+- name: html_marks
markdown: |-
- * list item 1
- * list item 2
- * embedded list item 3
-- name: bullet_list_style_2
+ * Content editor is ~~great~~<ins>amazing</ins>.
+ * If the changes <abbr title="Looks good to merge">LGTM</abbr>, please <abbr title="Merge when pipeline succeeds">MWPS</abbr>.
+ * The English song <q>Oh I do like to be beside the seaside</q> looks like this in Hebrew: <span dir="rtl">אה, אני אוהב להיות ליד חוף הים</span>. In the computer's memory, this is stored as <bdo dir="ltr">אה, אני אוהב להיות ליד חוף הים</bdo>.
+ * <cite>The Scream</cite> by Edvard Munch. Painted in 1893.
+ * <dfn>HTML</dfn> is the standard markup language for creating web pages.
+ * Do not forget to buy <mark>milk</mark> today.
+ * This is a paragraph and <small>smaller text goes here</small>.
+ * The concert starts at <time datetime="20:00">20:00</time> and you'll be able to enjoy the band for at least <time datetime="PT2H30M">2h 30m</time>.
+ * Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text (Windows).
+ * WWF's goal is to: <q>Build a future where people live in harmony with nature.</q> We hope they succeed.
+ * The error occured was: <samp>Keyboard not found. Press F1 to continue.</samp>
+ * The area of a triangle is: 1/2 x <var>b</var> x <var>h</var>, where <var>b</var> is the base, and <var>h</var> is the vertical height.
+ * <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
+ * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
+ * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
+- name: image
+ markdown: '![alt text](https://gitlab.com/logo.png)'
+- name: inline_code
+ markdown: '`code`'
+- name: inline_diff
markdown: |-
- - list item 1
- - list item 2
- * embedded list item 3
-- name: bullet_list_style_3
+ * {-deleted-}
+ * {+added+}
+- name: link
+ markdown: '[GitLab](https://gitlab.com)'
+- name: math
markdown: |-
- + list item 1
- + list item 2
- - embedded list item 3
+ This math is inline $`a^2+b^2=c^2`$.
+
+ This is on a separate line:
+
+ ```math
+ a^2+b^2=c^2
+ ```
- name: ordered_list
markdown: |-
1. list item 1
@@ -177,14 +230,6 @@
134. list item 1
135. list item 2
136. list item 3
-- name: task_list
- markdown: |-
- * [x] hello
- * [x] world
- * [ ] example
- * [ ] of nested
- * [x] task list
- * [ ] items
- name: ordered_task_list
markdown: |-
1. [x] hello
@@ -198,12 +243,12 @@
4893. [x] hello
4894. [x] world
4895. [ ] example
-- name: image
- markdown: '![alt text](https://gitlab.com/logo.png)'
-- name: hard_break
+- name: reference
+ context: project_wiki
markdown: |-
- This is a line after a\
- hard break
+ Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
+- name: strike
+ markdown: '~~del~~'
- name: table
markdown: |-
| header | header |
@@ -212,27 +257,6 @@
| ~~strike~~ | cell with _italic_ |
# content after table
-- name: emoji
- markdown: ':sparkles: :heart: :100:'
-- name: reference
- context: project_wiki
- markdown: |-
- Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
-- name: audio
- markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
-- name: video
- markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)'
-- name: audio_and_video_in_lists
- markdown: |-
- * ![Sample Audio](https://gitlab.com/1.mp3)
- * ![Sample Video](https://gitlab.com/2.mp4)
-
- 1. ![Sample Video](https://gitlab.com/1.mp4)
- 2. ![Sample Audio](https://gitlab.com/2.mp3)
-
- * [x] ![Sample Audio](https://gitlab.com/1.mp3)
- * [x] ![Sample Audio](https://gitlab.com/2.mp3)
- * [x] ![Sample Video](https://gitlab.com/3.mp4)
- name: table_of_contents
markdown: |-
[[_TOC_]]
@@ -248,42 +272,18 @@
# Sit amit
### I don't know
-- name: word_break
- markdown: Fernstraßen<wbr>bau<wbr>privat<wbr>finanzierungs<wbr>gesetz
-- name: frontmatter_yaml
- markdown: |-
- ---
- title: Page title
- ---
-- name: frontmatter_toml
- markdown: |-
- +++
- title = "Page title"
- +++
-- name: frontmatter_json
- markdown: |-
- ;;;
- {
- "title": "Page title"
- }
- ;;;
-- name: color_chips
+- name: task_list
markdown: |-
- - `#F00`
- - `#F00A`
- - `#FF0000`
- - `#FF0000AA`
- - `RGB(0,255,0)`
- - `RGB(0%,100%,0%)`
- - `RGBA(0,255,0,0.3)`
- - `HSL(540,70%,50%)`
- - `HSLA(540,70%,50%,0.3)`
-- name: math
+ * [x] hello
+ * [x] world
+ * [ ] example
+ * [ ] of nested
+ * [x] task list
+ * [ ] items
+- name: thematic_break
markdown: |-
- This math is inline $`a^2+b^2=c^2`$.
-
- This is on a separate line:
-
- ```math
- a^2+b^2=c^2
- ```
+ ---
+- name: video
+ markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)'
+- name: word_break
+ markdown: Fernstraßen<wbr>bau<wbr>privat<wbr>finanzierungs<wbr>gesetz
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 3c8964d398a..23c18c97df2 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -65,5 +65,31 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
expect_graphql_errors_to_be_empty
end
end
+
+ context 'project storage count query' do
+ before do
+ project.statistics.update!(
+ repository_size: 3900000,
+ lfs_objects_size: 4800000,
+ build_artifacts_size: 400000,
+ pipeline_artifacts_size: 400000,
+ wiki_size: 300000,
+ packages_size: 3800000,
+ uploads_size: 900000
+ )
+ end
+
+ base_input_path = 'projects/storage_counter/queries/'
+ base_output_path = 'graphql/projects/storage_counter/'
+ query_name = 'project_storage.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
end
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 96e5202780b..f7bde8d2f16 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -3,6 +3,7 @@ import createFlash, {
createAction,
hideFlash,
removeFlashClickListener,
+ FLASH_CLOSED_EVENT,
} from '~/flash';
describe('Flash', () => {
@@ -79,6 +80,16 @@ describe('Flash', () => {
expect(el.remove.mock.calls.length).toBe(1);
});
+
+ it(`dispatches ${FLASH_CLOSED_EVENT} event after transitionend event`, () => {
+ jest.spyOn(el, 'dispatchEvent');
+
+ hideFlash(el);
+
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(el.dispatchEvent).toHaveBeenCalledWith(new Event(FLASH_CLOSED_EVENT));
+ });
});
describe('createAction', () => {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index eb11df2fe43..631e3307f7f 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -2,7 +2,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
-import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
+import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete';
import { initEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import { TEST_HOST } from 'helpers/test_constants';
@@ -858,4 +858,14 @@ describe('GfmAutoComplete', () => {
);
});
});
+
+ describe('highlighter', () => {
+ it('escapes regex', () => {
+ const li = '<li>couple (woman,woman) <gl-emoji data-name="couple_ww"></gl-emoji></li>';
+
+ expect(highlighter(li, ')')).toBe(
+ '<li> couple (woman,woman<strong>)</strong> <gl-emoji data-name="couple_ww"></gl-emoji></li>',
+ );
+ });
+ });
});
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
new file mode 100644
index 00000000000..bb86eb5c22e
--- /dev/null
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlTab, GlTabs } from '@gitlab/ui';
+import App from '~/google_cloud/components/app.vue';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+import ServiceAccounts from '~/google_cloud/components/service_accounts.vue';
+
+describe('google_cloud App component', () => {
+ let wrapper;
+
+ const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findTabItems = () => findTabs().findAllComponents(GlTab);
+ const findConfigurationTab = () => findTabItems().at(0);
+ const findDeploymentTab = () => findTabItems().at(1);
+ const findServicesTab = () => findTabItems().at(2);
+ const findServiceAccounts = () => findConfigurationTab().findComponent(ServiceAccounts);
+
+ beforeEach(() => {
+ const propsData = {
+ serviceAccounts: [{}, {}],
+ createServiceAccountUrl: '#url-create-service-account',
+ emptyIllustrationUrl: '#url-empty-illustration',
+ };
+ wrapper = shallowMount(App, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should contain incubation banner', () => {
+ expect(findIncubationBanner().exists()).toBe(true);
+ });
+
+ describe('google_cloud App tabs', () => {
+ it('should contain tabs', () => {
+ expect(findTabs().exists()).toBe(true);
+ });
+
+ it('should contain three tab items', () => {
+ expect(findTabItems().length).toBe(3);
+ });
+
+ describe('configuration tab', () => {
+ it('should exist', () => {
+ expect(findConfigurationTab().exists()).toBe(true);
+ });
+
+ it('should contain service accounts component', () => {
+ expect(findServiceAccounts().exists()).toBe(true);
+ });
+ });
+
+ describe('deployments tab', () => {
+ it('should exist', () => {
+ expect(findDeploymentTab().exists()).toBe(true);
+ });
+ });
+
+ describe('services tab', () => {
+ it('should exist', () => {
+ expect(findServicesTab().exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js
new file mode 100644
index 00000000000..89517be4ef1
--- /dev/null
+++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js
@@ -0,0 +1,60 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert, GlLink } from '@gitlab/ui';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+
+describe('IncubationBanner component', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findFeatureRequestLink = () => findLinks().at(0);
+ const findReportBugLink = () => findLinks().at(1);
+ const findShareFeedbackLink = () => findLinks().at(2);
+
+ beforeEach(() => {
+ const propsData = {
+ shareFeedbackUrl: 'url_general_feedback',
+ reportBugUrl: 'url_report_bug',
+ featureRequestUrl: 'url_feature_request',
+ };
+ wrapper = mount(IncubationBanner, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('contains relevant text', () => {
+ expect(findAlert().text()).toContain(
+ 'This is an experimental feature developed by GitLab Incubation Engineering.',
+ );
+ });
+
+ describe('has relevant gl-links', () => {
+ it('three in total', () => {
+ expect(findLinks().length).toBe(3);
+ });
+
+ it('contains feature request link', () => {
+ const link = findFeatureRequestLink();
+ expect(link.text()).toBe('request a feature');
+ expect(link.attributes('href')).toBe('url_feature_request');
+ });
+
+ it('contains report bug link', () => {
+ const link = findReportBugLink();
+ expect(link.text()).toBe('report a bug');
+ expect(link.attributes('href')).toBe('url_report_bug');
+ });
+
+ it('contains share feedback link', () => {
+ const link = findShareFeedbackLink();
+ expect(link.text()).toBe('share feedback');
+ expect(link.attributes('href')).toBe('url_general_feedback');
+ });
+ });
+});
diff --git a/spec/frontend/google_cloud/components/service_accounts_spec.js b/spec/frontend/google_cloud/components/service_accounts_spec.js
new file mode 100644
index 00000000000..3d097078f03
--- /dev/null
+++ b/spec/frontend/google_cloud/components/service_accounts_spec.js
@@ -0,0 +1,79 @@
+import { mount } from '@vue/test-utils';
+import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import ServiceAccounts from '~/google_cloud/components/service_accounts.vue';
+
+describe('ServiceAccounts component', () => {
+ describe('when the project does not have any service accounts', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findButtonInEmptyState = () => findEmptyState().findComponent(GlButton);
+
+ beforeEach(() => {
+ const propsData = {
+ list: [],
+ createUrl: '#create-url',
+ emptyIllustrationUrl: '#empty-illustration-url',
+ };
+ wrapper = mount(ServiceAccounts, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows the empty state component', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ it('shows the link to create new service accounts', () => {
+ const button = findButtonInEmptyState();
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Create service account');
+ expect(button.attributes('href')).toBe('#create-url');
+ });
+ });
+
+ describe('when three service accounts are passed via props', () => {
+ let wrapper;
+
+ const findTitle = () => wrapper.find('h2');
+ const findDescription = () => wrapper.find('p');
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findRows = () => findTable().findAll('tr');
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ const propsData = {
+ list: [{}, {}, {}],
+ createUrl: '#create-url',
+ emptyIllustrationUrl: '#empty-illustration-url',
+ };
+ wrapper = mount(ServiceAccounts, { propsData });
+ });
+
+ it('shows the title', () => {
+ expect(findTitle().text()).toBe('Service Accounts');
+ });
+
+ it('shows the description', () => {
+ expect(findDescription().text()).toBe(
+ 'Service Accounts keys authorize GitLab to deploy your Google Cloud project',
+ );
+ });
+
+ it('shows the table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('table must have three rows + header row', () => {
+ expect(findRows().length).toBe(4);
+ });
+
+ it('shows the link to create new service accounts', () => {
+ const button = findButton();
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Create service account');
+ expect(button.attributes('href')).toBe('#create-url');
+ });
+ });
+});
diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js
index 1732f24eeff..9f478eedbfb 100644
--- a/spec/frontend/graphql_shared/utils_spec.js
+++ b/spec/frontend/graphql_shared/utils_spec.js
@@ -52,6 +52,10 @@ describe('getIdFromGraphQLId', () => {
output: null,
},
{
+ input: 'gid://gitlab/Environments/0',
+ output: 0,
+ },
+ {
input: 'gid://gitlab/Environments/123',
output: 123,
},
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 78950a8fe20..617d91178e4 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -3,13 +3,16 @@ import { shallowMount } from '@vue/test-utils';
import MockAxiosAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
-import { ENABLED, DISABLED, ALLOW_OVERRIDE } from '~/group_settings/constants';
import axios from '~/lib/utils/axios_utils';
-const TEST_UPDATE_PATH = '/test/update';
-const DISABLED_PAYLOAD = { shared_runners_setting: DISABLED };
-const ENABLED_PAYLOAD = { shared_runners_setting: ENABLED };
-const OVERRIDE_PAYLOAD = { shared_runners_setting: ALLOW_OVERRIDE };
+const provide = {
+ updatePath: '/test/update',
+ sharedRunnersAvailability: 'enabled',
+ parentSharedRunnersAvailability: null,
+ runnerDisabled: 'disabled',
+ runnerEnabled: 'enabled',
+ runnerAllowOverride: 'allow_override',
+};
jest.mock('~/flash');
@@ -17,13 +20,11 @@ describe('group_settings/components/shared_runners_form', () => {
let wrapper;
let mock;
- const createComponent = (props = {}) => {
+ const createComponent = (provides = {}) => {
wrapper = shallowMount(SharedRunnersForm, {
- propsData: {
- updatePath: TEST_UPDATE_PATH,
- sharedRunnersAvailability: ENABLED,
- parentSharedRunnersAvailability: null,
- ...props,
+ provide: {
+ ...provide,
+ ...provides,
},
});
};
@@ -33,13 +34,13 @@ describe('group_settings/components/shared_runners_form', () => {
const findEnabledToggle = () => wrapper.find('[data-testid="enable-runners-toggle"]');
const findOverrideToggle = () => wrapper.find('[data-testid="override-runners-toggle"]');
const changeToggle = (toggle) => toggle.vm.$emit('change', !toggle.props('value'));
- const getRequestPayload = () => JSON.parse(mock.history.put[0].data);
+ const getSharedRunnersSetting = () => JSON.parse(mock.history.put[0].data).shared_runners_setting;
const isLoadingIconVisible = () => findLoadingIcon().exists();
beforeEach(() => {
mock = new MockAxiosAdapter(axios);
- mock.onPut(TEST_UPDATE_PATH).reply(200);
+ mock.onPut(provide.updatePath).reply(200);
});
afterEach(() => {
@@ -95,7 +96,7 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(ENABLED_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerEnabled);
expect(findOverrideToggle().exists()).toBe(false);
});
@@ -104,14 +105,14 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(DISABLED_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerDisabled);
expect(findOverrideToggle().exists()).toBe(true);
});
});
describe('override toggle', () => {
beforeEach(() => {
- createComponent({ sharedRunnersAvailability: ALLOW_OVERRIDE });
+ createComponent({ sharedRunnersAvailability: provide.runnerAllowOverride });
});
it('enabling the override toggle sends correct payload', async () => {
@@ -119,7 +120,7 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(OVERRIDE_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerAllowOverride);
});
it('disabling the override toggle sends correct payload', async () => {
@@ -127,21 +128,21 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(DISABLED_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerDisabled);
});
});
describe('toggle disabled state', () => {
- it(`toggles are not disabled with setting ${DISABLED}`, () => {
- createComponent({ sharedRunnersAvailability: DISABLED });
+ it(`toggles are not disabled with setting ${provide.runnerDisabled}`, () => {
+ createComponent({ sharedRunnersAvailability: provide.runnerDisabled });
expect(findEnabledToggle().props('disabled')).toBe(false);
expect(findOverrideToggle().props('disabled')).toBe(false);
});
it('toggles are disabled', () => {
createComponent({
- sharedRunnersAvailability: DISABLED,
- parentSharedRunnersAvailability: DISABLED,
+ sharedRunnersAvailability: provide.runnerDisabled,
+ parentSharedRunnersAvailability: provide.runnerDisabled,
});
expect(findEnabledToggle().props('disabled')).toBe(true);
expect(findOverrideToggle().props('disabled')).toBe(true);
@@ -154,7 +155,7 @@ describe('group_settings/components/shared_runners_form', () => {
${{ error: 'Undefined error' }} | ${'Undefined error Refresh the page and try again.'}
`(`with error $errorObj`, ({ errorObj, message }) => {
beforeEach(async () => {
- mock.onPut(TEST_UPDATE_PATH).reply(500, errorObj);
+ mock.onPut(provide.updatePath).reply(500, errorObj);
createComponent();
changeToggle(findEnabledToggle());
diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
index 194a619c4aa..47e3a56e83d 100644
--- a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
@@ -8,7 +8,7 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli
<empty-state-stub
cansetci="true"
- class="mb-auto mt-auto"
+ class="gl-p-5"
emptystatesvgpath="http://test.host"
/>
</div>
diff --git a/spec/frontend/ide/components/shared/commit_message_field_spec.js b/spec/frontend/ide/components/shared/commit_message_field_spec.js
new file mode 100644
index 00000000000..f4f9b95b233
--- /dev/null
+++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js
@@ -0,0 +1,149 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CommitMessageField from '~/ide/components/shared/commit_message_field.vue';
+
+const DEFAULT_PROPS = {
+ text: 'foo text',
+ placeholder: 'foo placeholder',
+};
+
+describe('CommitMessageField', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(CommitMessageField, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ attachTo: document.body,
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTextArea = () => wrapper.find('textarea');
+ const findHighlights = () => wrapper.findByTestId('highlights');
+ const findHighlightsText = () => wrapper.findByTestId('highlights-text');
+ const findHighlightsMark = () => wrapper.findByTestId('highlights-mark');
+ const findHighlightsTexts = () => wrapper.findAllByTestId('highlights-text');
+ const findHighlightsMarks = () => wrapper.findAllByTestId('highlights-mark');
+
+ const fillText = async (text) => {
+ wrapper.setProps({ text });
+ await nextTick();
+ };
+
+ it('emits input event on input', () => {
+ const value = 'foo';
+
+ createComponent();
+ findTextArea().setValue(value);
+ expect(wrapper.emitted('input')[0][0]).toEqual(value);
+ });
+
+ describe('focus classes', () => {
+ beforeEach(async () => {
+ createComponent();
+ findTextArea().trigger('focus');
+ await nextTick();
+ });
+
+ it('is added on textarea focus', async () => {
+ expect(wrapper.attributes('class')).toEqual(
+ expect.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
+ );
+ });
+
+ it('is removed on textarea blur', async () => {
+ findTextArea().trigger('blur');
+ await nextTick();
+
+ expect(wrapper.attributes('class')).toEqual(
+ expect.not.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
+ );
+ });
+ });
+
+ describe('highlights', () => {
+ describe('subject line', () => {
+ it('does not highlight less than 50 characters', async () => {
+ const text = 'text less than 50 chars';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsText().text()).toEqual(text);
+ expect(findHighlightsMark().text()).toBeFalsy();
+ });
+
+ it('highlights characters over 50 length', async () => {
+ const text =
+ 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsText().text()).toEqual(text.slice(0, 50));
+ expect(findHighlightsMark().text()).toEqual(text.slice(50));
+ });
+ });
+
+ describe('body text', () => {
+ it('does not highlight body text less tan 72 characters', async () => {
+ const text = 'subject line\nbody content';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsTexts()).toHaveLength(2);
+ expect(findHighlightsMarks().at(1).attributes('style')).toEqual('display: none;');
+ });
+
+ it('highlights body text more than 72 characters', async () => {
+ const text =
+ 'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsTexts()).toHaveLength(2);
+ expect(findHighlightsMarks().at(1).attributes('style')).not.toEqual('display: none;');
+ expect(findHighlightsMarks().at(1).element.textContent).toEqual(' in length');
+ });
+
+ it('highlights body text & subject line', async () => {
+ const text =
+ 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsTexts()).toHaveLength(2);
+ expect(findHighlightsMarks()).toHaveLength(2);
+ expect(findHighlightsMarks().at(0).element.textContent).toEqual('d');
+ expect(findHighlightsMarks().at(1).element.textContent).toEqual(' in length');
+ });
+ });
+ });
+
+ describe('scrolling textarea', () => {
+ it('updates transform of highlights', async () => {
+ const yCoord = 50;
+
+ createComponent();
+ await fillText('subject line\n\n\n\n\n\n\n\n\n\n\nbody content');
+
+ wrapper.vm.$el.querySelector('textarea').scrollTo(0, yCoord);
+ await nextTick();
+
+ expect(wrapper.vm.scrollTop).toEqual(yCoord);
+ expect(findHighlights().attributes('style')).toEqual('transform: translate3d(0, -50px, 0);');
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 23fe23bdef9..4602a0837e0 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -86,12 +86,12 @@ describe('Multi-file store mutations', () => {
mutations.SET_EMPTY_STATE_SVGS(localState, {
emptyStateSvgPath: 'emptyState',
noChangesStateSvgPath: 'noChanges',
- committedStateSvgPath: 'commited',
+ committedStateSvgPath: 'committed',
});
expect(localState.emptyStateSvgPath).toBe('emptyState');
expect(localState.noChangesStateSvgPath).toBe('noChanges');
- expect(localState.committedStateSvgPath).toBe('commited');
+ expect(localState.committedStateSvgPath).toBe('committed');
});
});
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index f7aa0e889ea..1c1e1e7ebd4 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -24,14 +24,21 @@ describe('Import entities group dropdown component', () => {
});
it('passes namespaces from props to default slot', () => {
- const namespaces = ['ns1', 'ns2'];
+ const namespaces = [
+ { id: 1, fullPath: 'ns1' },
+ { id: 2, fullPath: 'ns2' },
+ ];
createComponent({ namespaces });
expect(namespacesTracker).toHaveBeenCalledWith({ namespaces });
});
it('filters namespaces based on user input', async () => {
- const namespaces = ['match1', 'some unrelated', 'match2'];
+ const namespaces = [
+ { id: 1, fullPath: 'match1' },
+ { id: 2, fullPath: 'some unrelated' },
+ { id: 3, fullPath: 'match2' },
+ ];
createComponent({ namespaces });
namespacesTracker.mockReset();
@@ -39,6 +46,11 @@ describe('Import entities group dropdown component', () => {
await nextTick();
- expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: ['match1', 'match2'] });
+ expect(namespacesTracker).toHaveBeenCalledWith({
+ namespaces: [
+ { id: 1, fullPath: 'match1' },
+ { id: 3, fullPath: 'match2' },
+ ],
+ });
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index 60f0780fdb3..cd56f573011 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -1,8 +1,6 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { STATUSES } from '~/import_entities/constants';
import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
-import { generateFakeEntry } from '../graphql/fixtures';
describe('import actions cell', () => {
let wrapper;
@@ -10,7 +8,9 @@ describe('import actions cell', () => {
const createComponent = (props) => {
wrapper = shallowMount(ImportActionsCell, {
propsData: {
- groupPathRegex: /^[a-zA-Z]+$/,
+ isFinished: false,
+ isAvailableForImport: false,
+ isInvalid: false,
...props,
},
});
@@ -20,10 +20,9 @@ describe('import actions cell', () => {
wrapper.destroy();
});
- describe('when import status is NONE', () => {
+ describe('when group is available for import', () => {
beforeEach(() => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
- createComponent({ group });
+ createComponent({ isAvailableForImport: true });
});
it('renders import button', () => {
@@ -37,10 +36,9 @@ describe('import actions cell', () => {
});
});
- describe('when import status is FINISHED', () => {
+ describe('when group is finished', () => {
beforeEach(() => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED });
- createComponent({ group });
+ createComponent({ isAvailableForImport: true, isFinished: true });
});
it('renders re-import button', () => {
@@ -58,29 +56,22 @@ describe('import actions cell', () => {
});
});
- it('does not render import button when group import is in progress', () => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.STARTED });
- createComponent({ group });
+ it('does not render import button when group is not available for import', () => {
+ createComponent({ isAvailableForImport: false });
const button = wrapper.findComponent(GlButton);
expect(button.exists()).toBe(false);
});
- it('renders import button as disabled when there are validation errors', () => {
- const group = generateFakeEntry({
- id: 1,
- status: STATUSES.NONE,
- validation_errors: [{ field: 'new_name', message: 'something ' }],
- });
- createComponent({ group });
+ it('renders import button as disabled when group is invalid', () => {
+ createComponent({ isInvalid: true, isAvailableForImport: true });
const button = wrapper.findComponent(GlButton);
expect(button.props().disabled).toBe(true);
});
it('emits import-group event when import button is clicked', () => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
- createComponent({ group });
+ createComponent({ isAvailableForImport: true });
const button = wrapper.findComponent(GlButton);
button.vm.$emit('click');
diff --git a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
index 2a56efd1cbb..f2735d86493 100644
--- a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
@@ -4,6 +4,11 @@ import { STATUSES } from '~/import_entities/constants';
import ImportSourceCell from '~/import_entities/import_groups/components/import_source_cell.vue';
import { generateFakeEntry } from '../graphql/fixtures';
+const generateFakeTableEntry = ({ flags = {}, ...entry }) => ({
+ ...generateFakeEntry(entry),
+ flags,
+});
+
describe('import source cell', () => {
let wrapper;
let group;
@@ -23,14 +28,14 @@ describe('import source cell', () => {
describe('when group status is NONE', () => {
beforeEach(() => {
- group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
createComponent({ group });
});
it('renders link to a group', () => {
const link = wrapper.findComponent(GlLink);
- expect(link.attributes().href).toBe(group.web_url);
- expect(link.text()).toContain(group.full_path);
+ expect(link.attributes().href).toBe(group.webUrl);
+ expect(link.text()).toContain(group.fullPath);
});
it('does not render last imported line', () => {
@@ -40,20 +45,24 @@ describe('import source cell', () => {
describe('when group status is FINISHED', () => {
beforeEach(() => {
- group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED });
+ group = generateFakeTableEntry({
+ id: 1,
+ status: STATUSES.FINISHED,
+ flags: {
+ isFinished: true,
+ },
+ });
createComponent({ group });
});
it('renders link to a group', () => {
const link = wrapper.findComponent(GlLink);
- expect(link.attributes().href).toBe(group.web_url);
- expect(link.text()).toContain(group.full_path);
+ expect(link.attributes().href).toBe(group.webUrl);
+ expect(link.text()).toContain(group.fullPath);
});
it('renders last imported line', () => {
- expect(wrapper.text()).toMatchInterpolatedText(
- 'fake_group_1 Last imported to root/last-group1',
- );
+ expect(wrapper.text()).toMatchInterpolatedText('fake_group_1 Last imported to root/group1');
});
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index f43e545e049..6e3df21e30a 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -1,39 +1,30 @@
-import {
- GlButton,
- GlEmptyState,
- GlLoadingIcon,
- GlSearchBoxByClick,
- GlDropdown,
- GlDropdownItem,
- GlTable,
-} from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
-import stubChildren from 'helpers/stub_children';
-import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import httpStatus from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
-import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
+import { i18n } from '~/import_entities/import_groups/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
-import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
-import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+jest.mock('~/flash');
+jest.mock('~/import_entities/import_groups/services/status_poller');
-const GlDropdownStub = stubComponent(GlDropdown, {
- template: '<div><h1 ref="text"><slot name="button-content"></slot></h1><slot></slot></div>',
-});
+Vue.use(VueApollo);
describe('import table', () => {
let wrapper;
let apolloProvider;
+ let axiosMock;
const SOURCE_URL = 'https://demo.host';
const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
@@ -44,76 +35,81 @@ describe('import table', () => {
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const findImportSelectedButton = () =>
- wrapper.findAllComponents(GlButton).wrappers.find((w) => w.text() === 'Import selected');
- const findPaginationDropdown = () => wrapper.findComponent(GlDropdown);
- const findPaginationDropdownText = () => findPaginationDropdown().find({ ref: 'text' }).text();
+ wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected');
+ const findImportButtons = () =>
+ wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
+ const findPaginationDropdown = () => wrapper.find('[aria-label="Page size"]');
+ const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
- // TODO: remove this ugly approach when
- // issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
- const findTable = () => wrapper.vm.getTableRef();
+ const selectRow = (idx) =>
+ wrapper.findAll('tbody td input[type=checkbox]').at(idx).trigger('click');
- const createComponent = ({ bulkImportSourceGroups }) => {
+ const createComponent = ({ bulkImportSourceGroups, importGroups }) => {
apolloProvider = createMockApollo([], {
Query: {
availableNamespaces: () => availableNamespacesFixture,
bulkImportSourceGroups,
},
Mutation: {
- setTargetNamespace: jest.fn(),
- setNewName: jest.fn(),
- importGroup: jest.fn(),
+ importGroups,
},
});
wrapper = mount(ImportTable, {
propsData: {
groupPathRegex: /.*/,
+ jobsPath: '/fake_job_path',
sourceUrl: SOURCE_URL,
- groupUrlErrorMessage: 'Please choose a group URL with no special characters or spaces.',
- },
- stubs: {
- ...stubChildren(ImportTable),
- GlSprintf: false,
- GlDropdown: GlDropdownStub,
- GlTable: false,
},
- localVue,
apolloProvider,
});
};
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ axiosMock.onGet(/.*\/exists$/, () => []).reply(200);
+ });
+
afterEach(() => {
wrapper.destroy();
});
- it('renders loading icon while performing request', async () => {
- createComponent({
- bulkImportSourceGroups: () => new Promise(() => {}),
+ describe('loading state', () => {
+ it('renders loading icon while performing request', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => new Promise(() => {}),
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- await waitForPromises();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- });
+ it('does not renders loading icon when request is completed', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => [],
+ });
+ await waitForPromises();
- it('does not renders loading icon when request is completed', async () => {
- createComponent({
- bulkImportSourceGroups: () => [],
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
- await waitForPromises();
-
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
- it('renders message about empty state when no groups are available for import', async () => {
- createComponent({
- bulkImportSourceGroups: () => ({
- nodes: [],
- pageInfo: FAKE_PAGE_INFO,
- }),
- });
- await waitForPromises();
+ describe('empty state', () => {
+ it('renders message about empty state when no groups are available for import', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [],
+ pageInfo: FAKE_PAGE_INFO,
+ }),
+ });
+ await waitForPromises();
- expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import');
+ expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import');
+ });
});
it('renders import row for each group in response', async () => {
@@ -140,40 +136,51 @@ describe('import table', () => {
expect(wrapper.text()).not.toContain('Showing 1-0');
});
- describe('converts row events to mutation invocations', () => {
- beforeEach(() => {
- createComponent({
- bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
- });
- return waitForPromises();
+ it('invokes importGroups mutation when row button is clicked', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
});
- it.each`
- event | payload | mutation | variables
- ${'update-target-namespace'} | ${'new-namespace'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace', newName: 'group1' }}
- ${'update-new-name'} | ${'new-name'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'root', newName: 'new-name' }}
- `('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => {
- jest.spyOn(apolloProvider.defaultClient, 'mutate');
- wrapper.find(ImportTargetCell).vm.$emit(event, payload);
- await waitForPromises();
- expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
- mutation,
- variables,
- });
- });
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
- it('invokes importGroups mutation when row button is clicked', async () => {
- jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ await waitForPromises();
- wrapper.findComponent(ImportActionsCell).vm.$emit('import-group');
- await waitForPromises();
- expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [FAKE_GROUP.id] },
- });
+ await findImportButtons()[0].trigger('click');
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: importGroupsMutation,
+ variables: {
+ importRequests: [
+ {
+ newName: FAKE_GROUP.lastImportTarget.newName,
+ sourceGroupId: FAKE_GROUP.id,
+ targetNamespace: availableNamespacesFixture[0].fullPath,
+ },
+ ],
+ },
});
});
+ it('displays error if importing group fails', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
+ importGroups: () => {
+ throw new Error();
+ },
+ });
+
+ axiosMock.onPost('/import/bulk_imports.json').reply(httpStatus.BAD_REQUEST);
+
+ await waitForPromises();
+ await findImportButtons()[0].trigger('click');
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: i18n.ERROR_IMPORT,
+ }),
+ );
+ });
+
describe('pagination', () => {
const bulkImportSourceGroupsQueryMock = jest
.fn()
@@ -195,10 +202,10 @@ describe('import table', () => {
});
it('updates page size when selected in Dropdown', async () => {
- const otherOption = wrapper.findAllComponents(GlDropdownItem).at(1);
+ const otherOption = findPaginationDropdown().findAll('li p').at(1);
expect(otherOption.text()).toMatchInterpolatedText('50 items per page');
- otherOption.vm.$emit('click');
+ await otherOption.trigger('click');
await waitForPromises();
expect(findPaginationDropdownText()).toMatchInterpolatedText('50 items per page');
@@ -247,7 +254,11 @@ describe('import table', () => {
return waitForPromises();
});
- const findFilterInput = () => wrapper.find(GlSearchBoxByClick);
+ const setFilter = (value) => {
+ const input = wrapper.find('input[placeholder="Filter by source group"]');
+ input.setValue(value);
+ return input.trigger('keydown.enter');
+ };
it('properly passes filter to graphql query when search box is submitted', async () => {
createComponent({
@@ -256,7 +267,7 @@ describe('import table', () => {
await waitForPromises();
const FILTER_VALUE = 'foo';
- findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await setFilter(FILTER_VALUE);
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@@ -274,7 +285,7 @@ describe('import table', () => {
await waitForPromises();
const FILTER_VALUE = 'foo';
- findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await setFilter(FILTER_VALUE);
await waitForPromises();
expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo" from');
@@ -282,12 +293,14 @@ describe('import table', () => {
it('properly resets filter in graphql query when search box is cleared', async () => {
const FILTER_VALUE = 'foo';
- findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await setFilter(FILTER_VALUE);
await waitForPromises();
bulkImportSourceGroupsQueryMock.mockClear();
await apolloProvider.defaultClient.resetStore();
- findFilterInput().vm.$emit('clear');
+
+ await setFilter('');
+
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@@ -320,8 +333,8 @@ describe('import table', () => {
}),
});
await waitForPromises();
- wrapper.find(GlTable).vm.$emit('row-selected', [FAKE_GROUPS[0]]);
- await nextTick();
+
+ await selectRow(0);
expect(findImportSelectedButton().props().disabled).toBe(false);
});
@@ -337,7 +350,7 @@ describe('import table', () => {
});
await waitForPromises();
- findTable().selectRow(0);
+ await selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
@@ -348,7 +361,6 @@ describe('import table', () => {
generateFakeEntry({
id: 2,
status: STATUSES.NONE,
- validation_errors: [{ field: 'new_name', message: 'FAKE_VALIDATION_ERROR' }],
}),
];
@@ -360,9 +372,9 @@ describe('import table', () => {
});
await waitForPromises();
- // TODO: remove this ugly approach when
- // issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
- findTable().selectRow(0);
+ await wrapper.find('tbody input[aria-label="New name"]').setValue('');
+ jest.runOnlyPendingTimers();
+ await selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
@@ -384,15 +396,28 @@ describe('import table', () => {
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await waitForPromises();
- findTable().selectRow(0);
- findTable().selectRow(1);
+ await selectRow(0);
+ await selectRow(1);
await nextTick();
- findImportSelectedButton().vm.$emit('click');
+ await findImportSelectedButton().trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
- variables: { sourceGroupIds: [NEW_GROUPS[0].id, NEW_GROUPS[1].id] },
+ variables: {
+ importRequests: [
+ {
+ targetNamespace: availableNamespacesFixture[0].fullPath,
+ newName: NEW_GROUPS[0].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[0].id,
+ },
+ {
+ targetNamespace: availableNamespacesFixture[0].fullPath,
+ newName: NEW_GROUPS[1].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[1].id,
+ },
+ ],
+ },
});
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index be83a61841f..3c2367e22f5 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -3,20 +3,20 @@ import { shallowMount } from '@vue/test-utils';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
-import { availableNamespacesFixture } from '../graphql/fixtures';
-
-const getFakeGroup = (status) => ({
- web_url: 'https://fake.host/',
- full_path: 'fake_group_1',
- full_name: 'fake_name_1',
- import_target: {
- target_namespace: 'root',
- new_name: 'group1',
- },
- id: 1,
- validation_errors: [],
- progress: { status },
-});
+import { generateFakeEntry, availableNamespacesFixture } from '../graphql/fixtures';
+
+const generateFakeTableEntry = ({ flags = {}, ...config }) => {
+ const entry = generateFakeEntry(config);
+
+ return {
+ ...entry,
+ importTarget: {
+ targetNamespace: availableNamespacesFixture[0],
+ newName: entry.lastImportTarget.newName,
+ },
+ flags,
+ };
+};
describe('import target cell', () => {
let wrapper;
@@ -31,7 +31,6 @@ describe('import target cell', () => {
propsData: {
availableNamespaces: availableNamespacesFixture,
groupPathRegex: /.*/,
- groupUrlErrorMessage: 'Please choose a group URL with no special characters or spaces.',
...props,
},
});
@@ -44,11 +43,11 @@ describe('import target cell', () => {
describe('events', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.NONE);
+ group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
createComponent({ group });
});
- it('invokes $event', () => {
+ it('emits update-new-name when input value is changed', () => {
findNameInput().vm.$emit('input', 'demo');
expect(wrapper.emitted('update-new-name')).toBeDefined();
expect(wrapper.emitted('update-new-name')[0][0]).toBe('demo');
@@ -56,18 +55,23 @@ describe('import target cell', () => {
it('emits update-target-namespace when dropdown option is clicked', () => {
const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2);
- const dropdownItemText = dropdownItem.text();
dropdownItem.vm.$emit('click');
expect(wrapper.emitted('update-target-namespace')).toBeDefined();
- expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(dropdownItemText);
+ expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(availableNamespacesFixture[1]);
});
});
describe('when entity status is NONE', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.NONE);
+ group = generateFakeTableEntry({
+ id: 1,
+ status: STATUSES.NONE,
+ flags: {
+ isAvailableForImport: true,
+ },
+ });
createComponent({ group });
});
@@ -78,7 +82,7 @@ describe('import target cell', () => {
it('renders only no parent option if available namespaces list is empty', () => {
createComponent({
- group: getFakeGroup(STATUSES.NONE),
+ group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
availableNamespaces: [],
});
@@ -92,7 +96,7 @@ describe('import target cell', () => {
it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => {
createComponent({
- group: getFakeGroup(STATUSES.NONE),
+ group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
availableNamespaces: availableNamespacesFixture,
});
@@ -104,9 +108,12 @@ describe('import target cell', () => {
expect(rest).toHaveLength(availableNamespacesFixture.length);
});
- describe('when entity status is SCHEDULING', () => {
+ describe('when entity is not available for import', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.SCHEDULING);
+ group = generateFakeTableEntry({
+ id: 1,
+ flags: { isAvailableForImport: false },
+ });
createComponent({ group });
});
@@ -115,9 +122,9 @@ describe('import target cell', () => {
});
});
- describe('when entity status is FINISHED', () => {
+ describe('when entity is available for import', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.FINISHED);
+ group = generateFakeTableEntry({ id: 1, flags: { isAvailableForImport: true } });
createComponent({ group });
});
@@ -125,41 +132,4 @@ describe('import target cell', () => {
expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined);
});
});
-
- describe('validations', () => {
- it('reports invalid group name when name is not matching regex', () => {
- createComponent({
- group: {
- ...getFakeGroup(STATUSES.NONE),
- import_target: {
- target_namespace: 'root',
- new_name: 'very`bad`name',
- },
- },
- groupPathRegex: /^[a-zA-Z]+$/,
- });
-
- expect(wrapper.text()).toContain(
- 'Please choose a group URL with no special characters or spaces.',
- );
- });
-
- it('reports invalid group name if relevant validation error exists', async () => {
- const FAKE_ERROR_MESSAGE = 'fake error';
-
- createComponent({
- group: {
- ...getFakeGroup(STATUSES.NONE),
- validation_errors: [
- {
- field: 'new_name',
- message: FAKE_ERROR_MESSAGE,
- },
- ],
- },
- });
-
- expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE);
- });
- });
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index e1d65095888..f3447494578 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -2,32 +2,27 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'mock-apollo-client';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import {
clientTypenames,
createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory';
-import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
+import { LocalStorageCache } from '~/import_entities/import_groups/graphql/services/local_storage_cache';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
-import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
-import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql';
-import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
-import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
-import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/group_and_project.query.graphql';
-import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
jest.mock('~/flash');
-jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
- StatusPoller: jest.fn().mockImplementation(function mock() {
- this.startPolling = jest.fn();
+jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({
+ LocalStorageCache: jest.fn().mockImplementation(function mock() {
+ this.get = jest.fn();
+ this.set = jest.fn();
+ this.updateStatusByJobId = jest.fn();
}),
}));
@@ -38,13 +33,6 @@ const FAKE_ENDPOINTS = {
jobs: '/fake_jobs',
};
-const FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER = jest.fn().mockResolvedValue({
- data: {
- existingGroup: null,
- existingProject: null,
- },
-});
-
describe('Bulk import resolvers', () => {
let axiosMockAdapter;
let client;
@@ -58,14 +46,28 @@ describe('Bulk import resolvers', () => {
resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS, ...extraResolverArgs }),
});
- mockedClient.setRequestHandler(groupAndProjectQuery, FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER);
-
return mockedClient;
};
- beforeEach(() => {
+ let results;
+ beforeEach(async () => {
axiosMockAdapter = new MockAdapter(axios);
client = createClient();
+
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(
+ httpStatus.OK,
+ availableNamespacesFixture.map((ns) => ({
+ id: ns.id,
+ full_path: ns.fullPath,
+ })),
+ );
+
+ client.watchQuery({ query: bulkImportSourceGroupsQuery }).subscribe(({ data }) => {
+ results = data.bulkImportSourceGroups.nodes;
+ });
+
+ return waitForPromises();
});
afterEach(() => {
@@ -74,104 +76,41 @@ describe('Bulk import resolvers', () => {
describe('queries', () => {
describe('availableNamespaces', () => {
- let results;
-
+ let namespacesResults;
beforeEach(async () => {
- axiosMockAdapter
- .onGet(FAKE_ENDPOINTS.availableNamespaces)
- .reply(httpStatus.OK, availableNamespacesFixture);
-
const response = await client.query({ query: availableNamespacesQuery });
- results = response.data.availableNamespaces;
+ namespacesResults = response.data.availableNamespaces;
});
it('mirrors REST endpoint response fields', () => {
const extractRelevantFields = (obj) => ({ id: obj.id, full_path: obj.full_path });
- expect(results.map(extractRelevantFields)).toStrictEqual(
+ expect(namespacesResults.map(extractRelevantFields)).toStrictEqual(
availableNamespacesFixture.map(extractRelevantFields),
);
});
});
- describe('bulkImportSourceGroup', () => {
- beforeEach(async () => {
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
- axiosMockAdapter
- .onGet(FAKE_ENDPOINTS.availableNamespaces)
- .reply(httpStatus.OK, availableNamespacesFixture);
-
- return client.query({
- query: bulkImportSourceGroupsQuery,
- });
- });
-
- it('returns group', async () => {
- const { id } = statusEndpointFixture.importable_data[0];
- const {
- data: { bulkImportSourceGroup: group },
- } = await client.query({
- query: bulkImportSourceGroupQuery,
- variables: { id: id.toString() },
- });
-
- expect(group).toMatchObject(statusEndpointFixture.importable_data[0]);
- });
- });
-
describe('bulkImportSourceGroups', () => {
- let results;
-
- beforeEach(async () => {
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
- axiosMockAdapter
- .onGet(FAKE_ENDPOINTS.availableNamespaces)
- .reply(httpStatus.OK, availableNamespacesFixture);
- });
-
it('respects cached import state when provided by group manager', async () => {
- const FAKE_JOB_ID = '1';
- const FAKE_STATUS = 'DEMO_STATUS';
- const FAKE_IMPORT_TARGET = {
- new_name: 'test-name',
- target_namespace: 'test-namespace',
+ const [localStorageCache] = LocalStorageCache.mock.instances;
+ const CACHED_DATA = {
+ progress: {
+ id: 'DEMO',
+ status: 'cached',
+ },
};
- const TARGET_INDEX = 0;
+ localStorageCache.get.mockReturnValueOnce(CACHED_DATA);
- const clientWithMockedManager = createClient({
- GroupsManager: jest.fn().mockImplementation(() => ({
- getImportStateFromStorageByGroupId(groupId) {
- if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) {
- return {
- jobId: FAKE_JOB_ID,
- importState: {
- status: FAKE_STATUS,
- importTarget: FAKE_IMPORT_TARGET,
- },
- };
- }
-
- return null;
- },
- })),
- });
-
- const clientResponse = await clientWithMockedManager.query({
+ const updatedResults = await client.query({
query: bulkImportSourceGroupsQuery,
+ fetchPolicy: 'no-cache',
});
- const clientResults = clientResponse.data.bulkImportSourceGroups.nodes;
-
- expect(clientResults[TARGET_INDEX].import_target).toStrictEqual(FAKE_IMPORT_TARGET);
- expect(clientResults[TARGET_INDEX].progress.status).toBe(FAKE_STATUS);
- });
-
- it('populates each result instance with empty import_target when there are no available namespaces', async () => {
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(httpStatus.OK, []);
-
- const response = await client.query({ query: bulkImportSourceGroupsQuery });
- results = response.data.bulkImportSourceGroups.nodes;
- expect(results.every((r) => r.import_target.target_namespace === '')).toBe(true);
+ expect(updatedResults.data.bulkImportSourceGroups.nodes[0].progress).toStrictEqual({
+ __typename: clientTypenames.BulkImportProgress,
+ ...CACHED_DATA.progress,
+ });
});
describe('when called', () => {
@@ -181,37 +120,23 @@ describe('Bulk import resolvers', () => {
});
it('mirrors REST endpoint response fields', () => {
- const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
+ const MIRRORED_FIELDS = [
+ { from: 'id', to: 'id' },
+ { from: 'full_name', to: 'fullName' },
+ { from: 'full_path', to: 'fullPath' },
+ { from: 'web_url', to: 'webUrl' },
+ ];
expect(
results.every((r, idx) =>
MIRRORED_FIELDS.every(
- (field) => r[field] === statusEndpointFixture.importable_data[idx][field],
+ (field) => r[field.to] === statusEndpointFixture.importable_data[idx][field.from],
),
),
).toBe(true);
});
- it('populates each result instance with status default to none', () => {
- expect(results.every((r) => r.progress.status === STATUSES.NONE)).toBe(true);
- });
-
- it('populates each result instance with import_target defaulted to first available namespace', () => {
- expect(
- results.every(
- (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
- ),
- ).toBe(true);
- });
-
- it('starts polling when request completes', async () => {
- const [statusPoller] = StatusPoller.mock.instances;
- expect(statusPoller.startPolling).toHaveBeenCalled();
- });
-
- it('requests validation status when request completes', async () => {
- expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).not.toHaveBeenCalled();
- jest.runOnlyPendingTimers();
- expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalled();
+ it('populates each result instance with empty status', () => {
+ expect(results.every((r) => r.progress === null)).toBe(true);
});
});
@@ -223,6 +148,7 @@ describe('Bulk import resolvers', () => {
`(
'properly passes GraphQL variable $variable as REST $queryParam query parameter',
async ({ variable, queryParam, value }) => {
+ axiosMockAdapter.resetHistory();
await client.query({
query: bulkImportSourceGroupsQuery,
variables: { [variable]: value },
@@ -237,275 +163,61 @@ describe('Bulk import resolvers', () => {
});
describe('mutations', () => {
- const GROUP_ID = 1;
-
beforeEach(() => {
- client.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: {
- nodes: [
- {
- __typename: clientTypenames.BulkImportSourceGroup,
- id: GROUP_ID,
- progress: {
- id: `test-${GROUP_ID}`,
- status: STATUSES.NONE,
- },
- web_url: 'https://fake.host/1',
- full_path: 'fake_group_1',
- full_name: 'fake_name_1',
- import_target: {
- target_namespace: 'root',
- new_name: 'group1',
- },
- last_import_target: {
- target_namespace: 'root',
- new_name: 'group1',
- },
- validation_errors: [],
- },
- ],
- pageInfo: {
- page: 1,
- perPage: 20,
- total: 37,
- totalPages: 2,
- },
- },
- },
- });
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
});
- describe('setImportTarget', () => {
- it('updates group target namespace and name', async () => {
- const NEW_TARGET_NAMESPACE = 'target';
- const NEW_NAME = 'new';
-
- const {
- data: {
- setImportTarget: {
- id: idInResponse,
- import_target: { target_namespace: namespaceInResponse, new_name: newNameInResponse },
- },
- },
- } = await client.mutate({
- mutation: setImportTargetMutation,
- variables: {
- sourceGroupId: GROUP_ID,
- targetNamespace: NEW_TARGET_NAMESPACE,
- newName: NEW_NAME,
- },
- });
-
- expect(idInResponse).toBe(GROUP_ID);
- expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE);
- expect(newNameInResponse).toBe(NEW_NAME);
- });
-
- it('invokes validation', async () => {
- const NEW_TARGET_NAMESPACE = 'target';
- const NEW_NAME = 'new';
-
+ describe('importGroup', () => {
+ it('sets import status to CREATED when request completes', async () => {
await client.mutate({
- mutation: setImportTargetMutation,
+ mutation: importGroupsMutation,
variables: {
- sourceGroupId: GROUP_ID,
- targetNamespace: NEW_TARGET_NAMESPACE,
- newName: NEW_NAME,
+ importRequests: [
+ {
+ sourceGroupId: statusEndpointFixture.importable_data[0].id,
+ newName: 'test',
+ targetNamespace: 'root',
+ },
+ ],
},
});
- expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalledWith({
- fullPath: `${NEW_TARGET_NAMESPACE}/${NEW_NAME}`,
- });
- });
- });
-
- describe('importGroup', () => {
- it('sets status to SCHEDULING when request initiates', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(() => new Promise(() => {}));
-
- client.mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- });
- await waitForPromises();
-
- const {
- bulkImportSourceGroups: { nodes: intermediateResults },
- } = client.readQuery({
- query: bulkImportSourceGroupsQuery,
- });
-
- expect(intermediateResults[0].progress.status).toBe(STATUSES.SCHEDULING);
- });
-
- describe('when request completes', () => {
- let results;
-
- beforeEach(() => {
- client
- .watchQuery({
- query: bulkImportSourceGroupsQuery,
- fetchPolicy: 'cache-only',
- })
- .subscribe(({ data }) => {
- results = data.bulkImportSourceGroups.nodes;
- });
- });
-
- it('sets import status to CREATED when request completes', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
- await client.mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- });
- await waitForPromises();
-
- expect(results[0].progress.status).toBe(STATUSES.CREATED);
- });
-
- it('resets status to NONE if request fails', async () => {
- axiosMockAdapter
- .onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.INTERNAL_SERVER_ERROR);
-
- client
- .mutate({
- mutation: [importGroupsMutation],
- variables: { sourceGroupIds: [GROUP_ID] },
- })
- .catch(() => {});
- await waitForPromises();
-
- expect(results[0].progress.status).toBe(STATUSES.NONE);
- });
- });
-
- it('shows default error message when server error is not provided', async () => {
- axiosMockAdapter
- .onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.INTERNAL_SERVER_ERROR);
-
- client
- .mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- })
- .catch(() => {});
- await waitForPromises();
-
- expect(createFlash).toHaveBeenCalledWith({ message: 'Importing the group failed' });
- });
-
- it('shows provided error message when error is included in backend response', async () => {
- const CUSTOM_MESSAGE = 'custom message';
-
- axiosMockAdapter
- .onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.INTERNAL_SERVER_ERROR, { error: CUSTOM_MESSAGE });
-
- client
- .mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- })
- .catch(() => {});
- await waitForPromises();
-
- expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
+ await axios.waitForAll();
+ expect(results[0].progress.status).toBe(STATUSES.CREATED);
});
});
- it('setImportProgress updates group progress and sets import target', async () => {
+ it('updateImportStatus updates status', async () => {
const NEW_STATUS = 'dummy';
- const FAKE_JOB_ID = 5;
- const IMPORT_TARGET = {
- __typename: 'ClientBulkImportTarget',
- new_name: 'fake_name',
- target_namespace: 'fake_target',
- };
- const {
- data: {
- setImportProgress: { progress, last_import_target: lastImportTarget },
- },
- } = await client.mutate({
- mutation: setImportProgressMutation,
+ await client.mutate({
+ mutation: importGroupsMutation,
variables: {
- sourceGroupId: GROUP_ID,
- status: NEW_STATUS,
- jobId: FAKE_JOB_ID,
- importTarget: IMPORT_TARGET,
+ importRequests: [
+ {
+ sourceGroupId: statusEndpointFixture.importable_data[0].id,
+ newName: 'test',
+ targetNamespace: 'root',
+ },
+ ],
},
});
+ await axios.waitForAll();
+ await waitForPromises();
- expect(lastImportTarget).toStrictEqual(IMPORT_TARGET);
-
- expect(progress).toStrictEqual({
- __typename: clientTypenames.BulkImportProgress,
- id: FAKE_JOB_ID,
- status: NEW_STATUS,
- });
- });
+ const { id } = results[0].progress;
- it('updateImportStatus returns new status', async () => {
- const NEW_STATUS = 'dummy';
- const FAKE_JOB_ID = 5;
const {
data: { updateImportStatus: statusInResponse },
} = await client.mutate({
mutation: updateImportStatusMutation,
- variables: { id: FAKE_JOB_ID, status: NEW_STATUS },
+ variables: { id, status: NEW_STATUS },
});
expect(statusInResponse).toStrictEqual({
__typename: clientTypenames.BulkImportProgress,
- id: FAKE_JOB_ID,
+ id,
status: NEW_STATUS,
});
});
-
- it('addValidationError adds error to group', async () => {
- const FAKE_FIELD = 'some-field';
- const FAKE_MESSAGE = 'some-message';
- const {
- data: {
- addValidationError: { validation_errors: validationErrors },
- },
- } = await client.mutate({
- mutation: addValidationErrorMutation,
- variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
- });
-
- expect(validationErrors).toStrictEqual([
- {
- __typename: clientTypenames.BulkImportValidationError,
- field: FAKE_FIELD,
- message: FAKE_MESSAGE,
- },
- ]);
- });
-
- it('removeValidationError removes error from group', async () => {
- const FAKE_FIELD = 'some-field';
- const FAKE_MESSAGE = 'some-message';
-
- await client.mutate({
- mutation: addValidationErrorMutation,
- variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
- });
-
- const {
- data: {
- removeValidationError: { validation_errors: validationErrors },
- },
- } = await client.mutate({
- mutation: removeValidationErrorMutation,
- variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD },
- });
-
- expect(validationErrors).toStrictEqual([]);
- });
});
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
index d1bd52693b6..5f6f9987a8f 100644
--- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -1,24 +1,24 @@
+import { STATUSES } from '~/import_entities/constants';
import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
export const generateFakeEntry = ({ id, status, ...rest }) => ({
__typename: clientTypenames.BulkImportSourceGroup,
- web_url: `https://fake.host/${id}`,
- full_path: `fake_group_${id}`,
- full_name: `fake_name_${id}`,
- import_target: {
- target_namespace: 'root',
- new_name: `group${id}`,
- },
- last_import_target: {
- target_namespace: 'root',
- new_name: `last-group${id}`,
+ webUrl: `https://fake.host/${id}`,
+ fullPath: `fake_group_${id}`,
+ fullName: `fake_name_${id}`,
+ lastImportTarget: {
+ id,
+ targetNamespace: 'root',
+ newName: `group${id}`,
},
id,
- progress: {
- id: `test-${id}`,
- status,
- },
- validation_errors: [],
+ progress:
+ status === STATUSES.NONE || status === STATUSES.PENDING
+ ? null
+ : {
+ id,
+ status,
+ },
...rest,
});
@@ -51,9 +51,9 @@ export const statusEndpointFixture = {
],
};
-export const availableNamespacesFixture = [
- { id: 24, full_path: 'Commit451' },
- { id: 22, full_path: 'gitlab-org' },
- { id: 23, full_path: 'gnuwget' },
- { id: 25, full_path: 'jashkenas' },
-];
+export const availableNamespacesFixture = Object.freeze([
+ { id: 24, fullPath: 'Commit451' },
+ { id: 22, fullPath: 'gitlab-org' },
+ { id: 23, fullPath: 'gnuwget' },
+ { id: 25, fullPath: 'jashkenas' },
+]);
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js
new file mode 100644
index 00000000000..b44a2767ad8
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js
@@ -0,0 +1,61 @@
+import {
+ KEY,
+ LocalStorageCache,
+} from '~/import_entities/import_groups/graphql/services/local_storage_cache';
+
+describe('Local storage cache', () => {
+ let cache;
+ let storage;
+
+ beforeEach(() => {
+ storage = {
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ };
+
+ cache = new LocalStorageCache({ storage });
+ });
+
+ describe('storage management', () => {
+ const IMPORT_URL = 'http://fake.url';
+
+ it('loads state from storage on creation', () => {
+ expect(storage.getItem).toHaveBeenCalledWith(KEY);
+ });
+
+ it('saves to storage when set is called', () => {
+ const STORAGE_CONTENT = { fake: 'content ' };
+ cache.set(IMPORT_URL, STORAGE_CONTENT);
+ expect(storage.setItem).toHaveBeenCalledWith(
+ KEY,
+ JSON.stringify({ [IMPORT_URL]: STORAGE_CONTENT }),
+ );
+ });
+
+ it('updates status by job id', () => {
+ const CHANGED_STATUS = 'changed';
+ const JOB_ID = 2;
+
+ cache.set(IMPORT_URL, {
+ progress: {
+ id: JOB_ID,
+ status: 'original',
+ },
+ });
+
+ cache.updateStatusByJobId(JOB_ID, CHANGED_STATUS);
+
+ expect(storage.setItem).toHaveBeenCalledWith(
+ KEY,
+ JSON.stringify({
+ [IMPORT_URL]: {
+ progress: {
+ id: JOB_ID,
+ status: CHANGED_STATUS,
+ },
+ },
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
deleted file mode 100644
index f06babcb149..00000000000
--- a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import {
- KEY,
- SourceGroupsManager,
-} from '~/import_entities/import_groups/graphql/services/source_groups_manager';
-
-const FAKE_SOURCE_URL = 'http://demo.host';
-
-describe('SourceGroupsManager', () => {
- let manager;
- let storage;
-
- beforeEach(() => {
- storage = {
- getItem: jest.fn(),
- setItem: jest.fn(),
- };
-
- manager = new SourceGroupsManager({ storage, sourceUrl: FAKE_SOURCE_URL });
- });
-
- describe('storage management', () => {
- const IMPORT_ID = 1;
- const IMPORT_TARGET = { new_name: 'demo', target_namespace: 'foo' };
- const STATUS = 'FAKE_STATUS';
- const FAKE_GROUP = { id: 1, import_target: IMPORT_TARGET, status: STATUS };
-
- it('loads state from storage on creation', () => {
- expect(storage.getItem).toHaveBeenCalledWith(KEY);
- });
-
- it('saves to storage when createImportState is called', () => {
- const FAKE_STATUS = 'fake;';
- manager.createImportState(IMPORT_ID, { status: FAKE_STATUS, groups: [FAKE_GROUP] });
- const storedObject = JSON.parse(storage.setItem.mock.calls[0][1]);
- expect(Object.values(storedObject)[0]).toStrictEqual({
- status: FAKE_STATUS,
- groups: [
- {
- id: FAKE_GROUP.id,
- importTarget: IMPORT_TARGET,
- },
- ],
- });
- });
-
- it('updates storage when previous state is available', () => {
- const CHANGED_STATUS = 'changed';
-
- manager.createImportState(IMPORT_ID, { status: STATUS, groups: [FAKE_GROUP] });
-
- manager.updateImportProgress(IMPORT_ID, CHANGED_STATUS);
- const storedObject = JSON.parse(storage.setItem.mock.calls[1][1]);
- expect(Object.values(storedObject)[0]).toStrictEqual({
- status: CHANGED_STATUS,
- groups: [
- {
- id: FAKE_GROUP.id,
- importTarget: IMPORT_TARGET,
- },
- ],
- });
- });
- });
-});
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
index 9c47647c430..01f976562c6 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
@@ -2,19 +2,13 @@ import MockAdapter from 'axios-mock-adapter';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
-import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
+import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
jest.mock('visibilityjs');
jest.mock('~/flash');
jest.mock('~/lib/utils/poll');
-jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manager', () => ({
- SourceGroupsManager: jest.fn().mockImplementation(function mock() {
- this.setImportStatus = jest.fn();
- this.findByImportId = jest.fn();
- }),
-}));
const FAKE_POLL_PATH = '/fake/poll/path';
@@ -81,6 +75,7 @@ describe('Bulk import status poller', () => {
const [pollInstance] = Poll.mock.instances;
poller.startPolling();
+ await Promise.resolve();
expect(pollInstance.makeRequest).toHaveBeenCalled();
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 8d4ccab2a40..48545ffd2d6 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -78,6 +78,7 @@ describe('Incidents List', () => {
authorUsernameQuery: '',
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
+ canCreateIncident: true,
...provide,
},
stubs: {
@@ -105,21 +106,23 @@ describe('Incidents List', () => {
describe('empty state', () => {
const {
- emptyState: { title, emptyClosedTabTitle, description },
+ emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
} = 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}
+ statusFilter | all | closed | expectedTitle | canCreateIncident | expectedDescription
+ ${'all'} | ${2} | ${1} | ${title} | ${true} | ${description}
+ ${'open'} | ${2} | ${0} | ${title} | ${true} | ${description}
+ ${'closed'} | ${0} | ${0} | ${title} | ${true} | ${description}
+ ${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${true} | ${undefined}
+ ${'all'} | ${2} | ${1} | ${title} | ${false} | ${cannotCreateIncidentDescription}
`(
`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 }) => {
+ ({ statusFilter, all, closed, expectedTitle, expectedDescription, canCreateIncident }) => {
mountComponent({
data: { incidents: { list: [] }, incidentsCount: { all, closed }, statusFilter },
+ provide: { canCreateIncident },
loading: false,
});
expect(findEmptyState().exists()).toBe(true);
@@ -219,6 +222,15 @@ describe('Incidents List', () => {
expect(findCreateIncidentBtn().exists()).toBe(false);
});
+ it("doesn't show the button when user does not have incident creation permissions", () => {
+ mountComponent({
+ data: { incidents: { list: mockIncidents }, incidentsCount: {} },
+ provide: { canCreateIncident: false },
+ loading: false,
+ });
+ expect(findCreateIncidentBtn().exists()).toBe(false);
+ });
+
it('should track create new incident button', async () => {
findCreateIncidentBtn().vm.$emit('click');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index da8a2f41c1b..bf044e388ea 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -35,136 +35,145 @@ describe('DynamicField', () => {
const findGlFormTextarea = () => wrapper.findComponent(GlFormTextarea);
describe('template', () => {
- describe.each([
- [true, 'disabled', 'readonly'],
- [false, undefined, undefined],
- ])('dynamic field, when isInheriting = `%p`', (isInheriting, disabled, readonly) => {
- describe('type is checkbox', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'checkbox',
- },
- isInheriting,
- );
- });
+ describe.each`
+ isInheriting | disabled | readonly | checkboxLabel
+ ${true} | ${'disabled'} | ${'readonly'} | ${undefined}
+ ${false} | ${undefined} | ${undefined} | ${'Custom checkbox label'}
+ `(
+ 'dynamic field, when isInheriting = `%p`',
+ ({ isInheriting, disabled, readonly, checkboxLabel }) => {
+ describe('type is checkbox', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'checkbox',
+ checkboxLabel,
+ },
+ isInheriting,
+ );
+ });
- it(`renders GlFormCheckbox, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
- expect(findGlFormCheckbox().exists()).toBe(true);
- expect(findGlFormCheckbox().find('[type=checkbox]').attributes('disabled')).toBe(
- disabled,
- );
- });
+ it(`renders GlFormCheckbox, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
+ expect(findGlFormCheckbox().exists()).toBe(true);
+ expect(findGlFormCheckbox().find('[type=checkbox]').attributes('disabled')).toBe(
+ disabled,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormCheckbox with correct text content when checkboxLabel is ${checkboxLabel}`, () => {
+ expect(findGlFormCheckbox().text()).toBe(checkboxLabel ?? defaultProps.title);
+ });
- describe('type is select', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'select',
- choices: [
- ['all', 'All details'],
- ['standard', 'Standard'],
- ],
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
});
- it(`renders GlFormSelect, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
- expect(findGlFormSelect().exists()).toBe(true);
- expect(findGlFormSelect().findAll('option')).toHaveLength(2);
- expect(findGlFormSelect().find('select').attributes('disabled')).toBe(disabled);
- });
+ describe('type is select', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'select',
+ choices: [
+ ['all', 'All details'],
+ ['standard', 'Standard'],
+ ],
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormSelect, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
+ expect(findGlFormSelect().exists()).toBe(true);
+ expect(findGlFormSelect().findAll('option')).toHaveLength(2);
+ expect(findGlFormSelect().find('select').attributes('disabled')).toBe(disabled);
+ });
- describe('type is textarea', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'textarea',
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
});
- it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
- expect(findGlFormTextarea().exists()).toBe(true);
- expect(findGlFormTextarea().find('textarea').attributes('readonly')).toBe(readonly);
- });
+ describe('type is textarea', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'textarea',
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormTextarea().exists()).toBe(true);
+ expect(findGlFormTextarea().find('textarea').attributes('readonly')).toBe(readonly);
+ });
- describe('type is password', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'password',
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
});
- it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
- expect(findGlFormInput().exists()).toBe(true);
- expect(findGlFormInput().attributes('type')).toBe('password');
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
- });
+ describe('type is password', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'password',
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- });
- });
+ it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes('type')).toBe('password');
+ expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ });
- describe('type is text', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'text',
- required: true,
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ });
});
- it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
- expect(findGlFormInput().exists()).toBe(true);
- expect(findGlFormInput().attributes()).toMatchObject({
- type: 'text',
- id: 'service_project_url',
- name: 'service[project_url]',
- placeholder: defaultProps.placeholder,
- required: 'required',
+ describe('type is text', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'text',
+ required: true,
+ },
+ isInheriting,
+ );
});
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
- });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
+ it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes()).toMatchObject({
+ type: 'text',
+ id: 'service_project_url',
+ name: 'service[project_url]',
+ placeholder: defaultProps.placeholder,
+ required: 'required',
+ });
+ expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ });
});
- });
- });
+ },
+ );
describe('help text', () => {
it('renders description with help text', () => {
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 119afbfecfe..3a664b652ac 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,7 +1,10 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { GET_JIRA_ISSUE_TYPES_EVENT } from '~/integrations/constants';
+import {
+ GET_JIRA_ISSUE_TYPES_EVENT,
+ VALIDATE_INTEGRATION_FORM_EVENT,
+} from '~/integrations/constants';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store';
@@ -17,12 +20,17 @@ describe('JiraIssuesFields', () => {
upgradePlanPath: 'https://gitlab.com',
};
- const createComponent = ({ isInheriting = false, props, ...options } = {}) => {
+ const createComponent = ({
+ isInheriting = false,
+ mountFn = mountExtended,
+ props,
+ ...options
+ } = {}) => {
store = createStore({
defaultState: isInheriting ? {} : undefined,
});
- wrapper = mountExtended(JiraIssuesFields, {
+ wrapper = mountFn(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
store,
stubs: ['jira-issue-creation-vulnerabilities'],
@@ -38,12 +46,19 @@ describe('JiraIssuesFields', () => {
const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput);
+ const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
+ const findConflictWarning = () => wrapper.findByTestId('conflict-warning-text');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
+ const assertProjectKeyState = (expectedStateValue) => {
+ expect(findProjectKey().attributes('state')).toBe(expectedStateValue);
+ expect(findProjectKeyFormGroup().attributes('state')).toBe(expectedStateValue);
+ };
+
describe('template', () => {
describe.each`
showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
@@ -151,19 +166,18 @@ describe('JiraIssuesFields', () => {
});
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({ props: { gitlabIssuesEnabled: false } });
-
- expect(wrapper.text()).not.toContain(expectedText);
- });
+ it.each`
+ gitlabIssuesEnabled | scenario
+ ${true} | ${'displays conflict warning'}
+ ${false} | ${'does not display conflict warning'}
+ `(
+ '$scenario when `gitlabIssuesEnabled` is `$gitlabIssuesEnabled`',
+ ({ gitlabIssuesEnabled }) => {
+ createComponent({ props: { gitlabIssuesEnabled } });
+
+ expect(findConflictWarning().exists()).toBe(gitlabIssuesEnabled);
+ },
+ );
});
describe('Vulnerabilities creation', () => {
@@ -211,5 +225,44 @@ describe('JiraIssuesFields', () => {
expect(eventHubEmitSpy).toHaveBeenCalledWith(GET_JIRA_ISSUE_TYPES_EVENT);
});
});
+
+ describe('Project key input field', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ initialProjectKey: '',
+ initialEnableJiraIssues: true,
+ },
+ mountFn: shallowMountExtended,
+ });
+ });
+
+ it('sets Project Key `state` attribute to `true` by default', () => {
+ assertProjectKeyState('true');
+ });
+
+ describe('when event hub recieves `VALIDATE_INTEGRATION_FORM_EVENT` event', () => {
+ describe('with no project key', () => {
+ it('sets Project Key `state` attribute to `undefined`', async () => {
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ await wrapper.vm.$nextTick();
+
+ assertProjectKeyState(undefined);
+ });
+ });
+
+ describe('when project key is set', () => {
+ it('sets Project Key `state` attribute to `true`', async () => {
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+
+ // set the project key
+ await findProjectKey().vm.$emit('input', 'AB');
+ await wrapper.vm.$nextTick();
+
+ assertProjectKeyState('true');
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
index f8f3f0fd318..c35d178e518 100644
--- a/spec/frontend/integrations/integration_settings_form_spec.js
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -1,27 +1,38 @@
import MockAdaptor from 'axios-mock-adapter';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+import eventHub from '~/integrations/edit/event_hub';
import axios from '~/lib/utils/axios_utils';
import toast from '~/vue_shared/plugins/global_toast';
+import {
+ I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
+ I18N_SUCCESSFUL_CONNECTION_MESSAGE,
+ I18N_DEFAULT_ERROR_MESSAGE,
+ GET_JIRA_ISSUE_TYPES_EVENT,
+ TOGGLE_INTEGRATION_EVENT,
+ TEST_INTEGRATION_EVENT,
+ SAVE_INTEGRATION_EVENT,
+} from '~/integrations/constants';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/vue_shared/plugins/global_toast');
+jest.mock('lodash/delay', () => (callback) => callback());
+
+const FIXTURE = 'services/edit_service.html';
describe('IntegrationSettingsForm', () => {
- const FIXTURE = 'services/edit_service.html';
+ let integrationSettingsForm;
+
+ const mockStoreDispatch = () => jest.spyOn(integrationSettingsForm.vue.$store, 'dispatch');
beforeEach(() => {
loadFixtures(FIXTURE);
+
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
});
describe('constructor', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- jest.spyOn(integrationSettingsForm, 'init').mockImplementation(() => {});
- });
-
it('should initialize form element refs on class object', () => {
- // Form Reference
expect(integrationSettingsForm.$form).toBeDefined();
expect(integrationSettingsForm.$form.nodeName).toBe('FORM');
expect(integrationSettingsForm.formActive).toBeDefined();
@@ -32,180 +43,206 @@ describe('IntegrationSettingsForm', () => {
});
});
- describe('toggleServiceState', () => {
- let integrationSettingsForm;
+ describe('event handling', () => {
+ let mockAxios;
beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- });
-
- it('should remove `novalidate` attribute to form when called with `true`', () => {
- integrationSettingsForm.formActive = true;
- integrationSettingsForm.toggleServiceState();
-
- expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBe(null);
- });
-
- it('should set `novalidate` attribute to form when called with `false`', () => {
- integrationSettingsForm.formActive = false;
- integrationSettingsForm.toggleServiceState();
-
- expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBeDefined();
- });
- });
-
- describe('testSettings', () => {
- let integrationSettingsForm;
- let formData;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdaptor(axios);
-
+ mockAxios = new MockAdaptor(axios);
jest.spyOn(axios, 'put');
-
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- integrationSettingsForm.init();
-
- formData = new FormData(integrationSettingsForm.$form);
});
afterEach(() => {
- mock.restore();
+ mockAxios.restore();
+ eventHub.dispose(); // clear event hub handlers
});
- it('should make an ajax request with provided `formData`', async () => {
- await integrationSettingsForm.testSettings(formData);
+ describe('when event hub receives `TOGGLE_INTEGRATION_EVENT`', () => {
+ it('should remove `novalidate` attribute to form when called with `true`', () => {
+ eventHub.$emit(TOGGLE_INTEGRATION_EVENT, true);
- expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
- });
+ expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBe(null);
+ });
- it('should show success message if test is successful', async () => {
- jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
+ it('should set `novalidate` attribute to form when called with `false`', () => {
+ eventHub.$emit(TOGGLE_INTEGRATION_EVENT, false);
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: false,
+ expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBe('novalidate');
});
+ });
- await integrationSettingsForm.testSettings(formData);
+ describe('when event hub receives `TEST_INTEGRATION_EVENT`', () => {
+ describe('when form is valid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(true);
+ });
- expect(toast).toHaveBeenCalledWith('Connection successful.');
- });
+ it('should make an ajax request with provided `formData`', async () => {
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- it('should show error message if ajax request responds with test error', async () => {
- const errorMessage = 'Test failed.';
- const serviceResponse = 'some error';
+ expect(axios.put).toHaveBeenCalledWith(
+ integrationSettingsForm.testEndPoint,
+ new FormData(integrationSettingsForm.$form),
+ );
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: errorMessage,
- service_response: serviceResponse,
- test_failed: false,
- });
+ it('should show success message if test is successful', async () => {
+ jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
- await integrationSettingsForm.testSettings(formData);
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ });
- expect(toast).toHaveBeenCalledWith(`${errorMessage} ${serviceResponse}`);
- });
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- it('should show error message if ajax request failed', async () => {
- const errorMessage = 'Something went wrong on our end.';
+ expect(toast).toHaveBeenCalledWith(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+ it('should show error message if ajax request responds with test error', async () => {
+ const errorMessage = 'Test failed.';
+ const serviceResponse = 'some error';
- await integrationSettingsForm.testSettings(formData);
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ service_response: serviceResponse,
+ test_failed: false,
+ });
- expect(toast).toHaveBeenCalledWith(errorMessage);
- });
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- it('should always dispatch `setIsTesting` with `false` once request is completed', async () => {
- const dispatchSpy = jest.fn();
+ expect(toast).toHaveBeenCalledWith(`${errorMessage} ${serviceResponse}`);
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+ it('should show error message if ajax request failed', async () => {
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- await integrationSettingsForm.testSettings(formData);
+ expect(toast).toHaveBeenCalledWith(I18N_DEFAULT_ERROR_MESSAGE);
+ });
- expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
- });
- });
+ it('should always dispatch `setIsTesting` with `false` once request is completed', async () => {
+ const dispatchSpy = mockStoreDispatch();
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
- describe('getJiraIssueTypes', () => {
- let integrationSettingsForm;
- let formData;
- let mock;
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- beforeEach(() => {
- mock = new MockAdaptor(axios);
+ expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
+ });
+ });
- jest.spyOn(axios, 'put');
+ describe('when form is invalid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(false);
+ jest.spyOn(integrationSettingsForm, 'testSettings');
+ });
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- integrationSettingsForm.init();
+ it('should dispatch `setIsTesting` with `false` and not call `testSettings`', async () => {
+ const dispatchSpy = mockStoreDispatch();
- formData = new FormData(integrationSettingsForm.$form);
- });
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- afterEach(() => {
- mock.restore();
+ expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
+ expect(integrationSettingsForm.testSettings).not.toHaveBeenCalled();
+ });
+ });
});
- it('should always dispatch `requestJiraIssueTypes`', async () => {
- const dispatchSpy = jest.fn();
-
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+ describe('when event hub receives `GET_JIRA_ISSUE_TYPES_EVENT`', () => {
+ it('should always dispatch `requestJiraIssueTypes`', () => {
+ const dispatchSpy = mockStoreDispatch();
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
- await integrationSettingsForm.getJiraIssueTypes();
+ expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
+ });
- expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
- });
+ it('should make an ajax request with provided `formData`', () => {
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
- it('should make an ajax request with provided `formData`', async () => {
- await integrationSettingsForm.getJiraIssueTypes(formData);
+ expect(axios.put).toHaveBeenCalledWith(
+ integrationSettingsForm.testEndPoint,
+ new FormData(integrationSettingsForm.$form),
+ );
+ });
- expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
- });
+ it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
+ const dispatchSpy = mockStoreDispatch();
+ const mockData = ['ISSUE', 'EPIC'];
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ issuetypes: mockData,
+ });
- it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
- const mockData = ['ISSUE', 'EPIC'];
- const dispatchSpy = jest.fn();
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
+ await waitForPromises();
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: false,
- issuetypes: mockData,
+ expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
});
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
-
- await integrationSettingsForm.getJiraIssueTypes(formData);
+ it.each(['Custom error message here', undefined])(
+ 'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
+ async (responseErrorMessage) => {
+ const dispatchSpy = mockStoreDispatch();
+
+ const expectedErrorMessage =
+ responseErrorMessage || I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE;
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: responseErrorMessage,
+ });
+
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
+ await waitForPromises();
+
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ 'receiveJiraIssueTypesError',
+ expectedErrorMessage,
+ );
+ },
+ );
+ });
+
+ describe('when event hub receives `SAVE_INTEGRATION_EVENT`', () => {
+ describe('when form is valid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(true);
+ jest.spyOn(integrationSettingsForm.$form, 'submit');
+ });
- expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
- });
+ it('should submit the form', async () => {
+ eventHub.$emit(SAVE_INTEGRATION_EVENT);
+ await waitForPromises();
- it.each(['something went wrong', undefined])(
- 'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
- async (responseErrorMessage) => {
- const defaultErrorMessage = 'Connection failed. Please check your settings.';
- const expectedErrorMessage = responseErrorMessage || defaultErrorMessage;
- const dispatchSpy = jest.fn();
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalledTimes(1);
+ });
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: responseErrorMessage,
+ describe('when form is invalid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(false);
+ jest.spyOn(integrationSettingsForm.$form, 'submit');
});
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+ it('should dispatch `setIsSaving` with `false` and not submit form', async () => {
+ const dispatchSpy = mockStoreDispatch();
- await integrationSettingsForm.getJiraIssueTypes(formData);
+ eventHub.$emit(SAVE_INTEGRATION_EVENT);
- expect(dispatchSpy).toHaveBeenCalledWith(
- 'receiveJiraIssueTypesError',
- expectedErrorMessage,
- );
- },
- );
+ await waitForPromises();
+
+ expect(dispatchSpy).toHaveBeenCalledWith('setIsSaving', false);
+ expect(integrationSettingsForm.$form.submit).not.toHaveBeenCalled();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/invite_members/components/confetti_spec.js b/spec/frontend/invite_members/components/confetti_spec.js
new file mode 100644
index 00000000000..2f361f1dc1e
--- /dev/null
+++ b/spec/frontend/invite_members/components/confetti_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import confetti from 'canvas-confetti';
+import Confetti from '~/invite_members/components/confetti.vue';
+
+jest.mock('canvas-confetti', () => ({
+ create: jest.fn(),
+}));
+
+let wrapper;
+
+const createComponent = () => {
+ wrapper = shallowMount(Confetti);
+};
+
+afterEach(() => {
+ wrapper.destroy();
+});
+
+describe('Confetti', () => {
+ it('initiates confetti', () => {
+ const basicCannon = jest.spyOn(Confetti.methods, 'basicCannon').mockImplementation(() => {});
+
+ createComponent();
+
+ expect(confetti.create).toHaveBeenCalled();
+ expect(basicCannon).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 8c3c549a5eb..5be79004640 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -15,17 +15,34 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
+import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
-import { INVITE_MEMBERS_IN_COMMENT, MEMBER_AREAS_OF_FOCUS } from '~/invite_members/constants';
+import {
+ INVITE_MEMBERS_IN_COMMENT,
+ MEMBER_AREAS_OF_FOCUS,
+ INVITE_MEMBERS_FOR_TASK,
+ CANCEL_BUTTON_TEXT,
+ INVITE_BUTTON_TEXT,
+ MEMBERS_MODAL_CELEBRATE_INTRO,
+ MEMBERS_MODAL_CELEBRATE_TITLE,
+ MEMBERS_MODAL_DEFAULT_TITLE,
+ MEMBERS_PLACEHOLDER,
+ MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
+} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
+import { getParameterValues } from '~/lib/utils/url_utility';
import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
let wrapper;
let mock;
jest.mock('~/experimentation/experiment_tracking');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ getParameterValues: jest.fn(() => []),
+}));
const id = '1';
const name = 'test name';
@@ -40,6 +57,15 @@ const areasOfFocusOptions = [
{ text: 'area1', value: 'area1' },
{ text: 'area2', value: 'area2' },
];
+const tasksToBeDoneOptions = [
+ { text: 'First task', value: 'first' },
+ { text: 'Second task', value: 'second' },
+];
+const newProjectPath = 'projects/new';
+const projects = [
+ { text: 'First project', value: '1' },
+ { text: 'Second project', value: '2' },
+];
const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
@@ -56,9 +82,13 @@ const user4 = {
avatar_url: '',
};
const sharedGroup = { id: '981' };
+const GlEmoji = { template: '<img/>' };
const createComponent = (data = {}, props = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
+ provide: {
+ newProjectPath,
+ },
propsData: {
id,
name,
@@ -68,6 +98,8 @@ const createComponent = (data = {}, props = {}) => {
areasOfFocusOptions,
defaultAccessLevel,
noSelectionAreasOfFocus,
+ tasksToBeDoneOptions,
+ projects,
helpLink,
...props,
},
@@ -81,6 +113,7 @@ const createComponent = (data = {}, props = {}) => {
}),
GlDropdown: true,
GlDropdownItem: true,
+ GlEmoji,
GlSprintf,
GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback', 'description'],
@@ -131,6 +164,11 @@ describe('InviteMembersModal', () => {
const membersFormGroupDescription = () => findMembersFormGroup().props('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
+ const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
+ const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
+ const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
+ const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
+ const findCelebrationEmoji = () => wrapper.findComponent(GlModal).find(GlEmoji);
describe('rendering the modal', () => {
beforeEach(() => {
@@ -138,15 +176,15 @@ describe('InviteMembersModal', () => {
});
it('renders the modal with the correct title', () => {
- expect(wrapper.findComponent(GlModal).props('title')).toBe('Invite members');
+ expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_DEFAULT_TITLE);
});
it('renders the Cancel button text correctly', () => {
- expect(findCancelButton().text()).toBe('Cancel');
+ expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
});
it('renders the Invite button text correctly', () => {
- expect(findInviteButton().text()).toBe('Invite');
+ expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
});
it('renders the Invite button modal without isLoading', () => {
@@ -171,7 +209,7 @@ describe('InviteMembersModal', () => {
describe('rendering the access expiration date field', () => {
it('renders the datepicker', () => {
- expect(findDatepicker()).toExist();
+ expect(findDatepicker().exists()).toBe(true);
});
});
});
@@ -191,14 +229,164 @@ describe('InviteMembersModal', () => {
});
});
+ describe('rendering the tasks to be done', () => {
+ const setupComponent = (
+ extraData = {},
+ props = {},
+ urlParameter = ['invite_members_for_task'],
+ ) => {
+ const data = {
+ selectedAccessLevel: 30,
+ selectedTasksToBeDone: ['ci', 'code'],
+ ...extraData,
+ };
+ getParameterValues.mockImplementation(() => urlParameter);
+ createComponent(data, props);
+ };
+
+ afterAll(() => {
+ getParameterValues.mockImplementation(() => []);
+ });
+
+ it('renders the tasks to be done', () => {
+ setupComponent();
+
+ expect(findTasksToBeDone().exists()).toBe(true);
+ });
+
+ describe('when the selected access level is lower than 30', () => {
+ it('does not render the tasks to be done', () => {
+ setupComponent({ selectedAccessLevel: 20 });
+
+ expect(findTasksToBeDone().exists()).toBe(false);
+ });
+ });
+
+ describe('when the url does not contain the parameter `open_modal=invite_members_for_task`', () => {
+ it('does not render the tasks to be done', () => {
+ setupComponent({}, {}, []);
+
+ expect(findTasksToBeDone().exists()).toBe(false);
+ });
+ });
+
+ describe('rendering the tasks', () => {
+ it('renders the tasks', () => {
+ setupComponent();
+
+ expect(findTasks().exists()).toBe(true);
+ });
+
+ it('does not render an alert', () => {
+ setupComponent();
+
+ expect(findNoProjectsAlert().exists()).toBe(false);
+ });
+
+ describe('when there are no projects passed in the data', () => {
+ it('does not render the tasks', () => {
+ setupComponent({}, { projects: [] });
+
+ expect(findTasks().exists()).toBe(false);
+ });
+
+ it('renders an alert with a link to the new projects path', () => {
+ setupComponent({}, { projects: [] });
+
+ expect(findNoProjectsAlert().exists()).toBe(true);
+ expect(findNoProjectsAlert().findComponent(GlLink).attributes('href')).toBe(
+ newProjectPath,
+ );
+ });
+ });
+ });
+
+ describe('rendering the project dropdown', () => {
+ it('renders the project select', () => {
+ setupComponent();
+
+ expect(findProjectSelect().exists()).toBe(true);
+ });
+
+ describe('when the modal is shown for a project', () => {
+ it('does not render the project select', () => {
+ setupComponent({}, { isProject: true });
+
+ expect(findProjectSelect().exists()).toBe(false);
+ });
+ });
+
+ describe('when no tasks are selected', () => {
+ it('does not render the project select', () => {
+ setupComponent({ selectedTasksToBeDone: [] });
+
+ expect(findProjectSelect().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('tracking events', () => {
+ it('tracks the view for invite_members_for_task', () => {
+ setupComponent();
+
+ expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
+ expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
+ INVITE_MEMBERS_FOR_TASK.view,
+ );
+ });
+
+ it('tracks the submit for invite_members_for_task', () => {
+ setupComponent();
+ clickInviteButton();
+
+ expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, {
+ label: 'selected_tasks_to_be_done',
+ property: 'ci,code',
+ });
+ expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
+ INVITE_MEMBERS_FOR_TASK.submit,
+ );
+ });
+ });
+ });
+
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
- it('includes the correct invitee, type, and formatted name', () => {
+ beforeEach(() => {
createInviteMembersToProjectWrapper();
+ });
+ it('renders the modal without confetti', () => {
+ expect(wrapper.findComponent(ModalConfetti).exists()).toBe(false);
+ });
+
+ it('includes the correct invitee, type, and formatted name', () => {
expect(findIntroText()).toBe("You're inviting members to the test name project.");
- expect(membersFormGroupDescription()).toBe('Select members or type email addresses');
+ expect(findCelebrationEmoji().exists()).toBe(false);
+ expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
+ });
+ });
+
+ describe('when inviting members with celebration', () => {
+ beforeEach(() => {
+ createComponent({ mode: 'celebrate', inviteeType: 'members' }, { isProject: true });
+ });
+
+ it('renders the modal with confetti', () => {
+ expect(wrapper.findComponent(ModalConfetti).exists()).toBe(true);
+ });
+
+ it('renders the modal with the correct title', () => {
+ expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE);
+ });
+
+ it('includes the correct celebration text and emoji', () => {
+ expect(findIntroText()).toBe(
+ `${MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT} ${MEMBERS_MODAL_CELEBRATE_INTRO}`,
+ );
+ expect(findCelebrationEmoji().exists()).toBe(true);
+ expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
});
@@ -218,7 +406,7 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name group.");
- expect(membersFormGroupDescription()).toBe('Select members or type email addresses');
+ expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
});
@@ -267,6 +455,8 @@ describe('InviteMembersModal', () => {
invite_source: inviteSource,
format: 'json',
areas_of_focus: noSelectionAreasOfFocus,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
};
describe('when member is added successfully', () => {
@@ -448,6 +638,8 @@ describe('InviteMembersModal', () => {
email: 'email@example.com',
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
format: 'json',
};
@@ -576,6 +768,8 @@ describe('InviteMembersModal', () => {
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
format: 'json',
+ tasks_to_be_done: [],
+ tasks_project_id: '',
};
const emailPostData = { ...postData, email: 'email@example.com' };
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index b2ebb9e4a47..3fce23f854c 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -1,8 +1,9 @@
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
+import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '~/invite_members/constants';
jest.mock('~/experimentation/experiment_tracking');
@@ -15,6 +16,7 @@ let findButton;
const triggerComponent = {
button: GlButton,
anchor: GlLink,
+ 'side-nav': GlLink,
};
const createComponent = (props = {}) => {
@@ -27,9 +29,23 @@ const createComponent = (props = {}) => {
});
};
-describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement) => {
- triggerProps = { triggerElement, triggerSource };
- findButton = () => wrapper.findComponent(triggerComponent[triggerElement]);
+const triggerItems = [
+ {
+ triggerElement: TRIGGER_ELEMENT_BUTTON,
+ },
+ {
+ triggerElement: 'anchor',
+ },
+ {
+ triggerElement: TRIGGER_ELEMENT_SIDE_NAV,
+ icon: 'plus',
+ },
+];
+
+describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
+ triggerProps = { ...triggerItem, triggerSource };
+
+ findButton = () => wrapper.findComponent(triggerComponent[triggerItem.triggerElement]);
afterEach(() => {
wrapper.destroy();
@@ -91,3 +107,14 @@ describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement
});
});
});
+
+describe('side-nav with icon', () => {
+ it('includes the specified icon with correct size when triggerElement is link', () => {
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ createComponent({ triggerElement: TRIGGER_ELEMENT_SIDE_NAV, icon: 'plus' });
+
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('plus');
+ });
+});
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index 307323ef07a..f4636fd7e6a 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -1,7 +1,8 @@
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CsvImportModal from '~/issuable/components/csv_import_modal.vue';
+import { __ } from '~/locale';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -36,7 +37,6 @@ describe('CsvImportModal', () => {
});
const findModal = () => wrapper.findComponent(GlModal);
- const findPrimaryButton = () => wrapper.findComponent(GlButton);
const findForm = () => wrapper.find('form');
const findFileInput = () => wrapper.findByLabelText('Upload CSV file');
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
@@ -64,11 +64,11 @@ describe('CsvImportModal', () => {
expect(findForm().exists()).toBe(true);
expect(findForm().attributes('action')).toBe(importCsvIssuesPath);
expect(findAuthenticityToken()).toBe('mock-csrf-token');
- expect(findFileInput()).toExist();
+ expect(findFileInput().exists()).toBe(true);
});
it('displays the correct primary button action text', () => {
- expect(findPrimaryButton()).toExist();
+ expect(findModal().props('actionPrimary')).toEqual({ text: __('Import issues') });
});
it('submits the form when the primary action is clicked', () => {
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index bd05cb1ac5a..e32215b4aa6 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -8,7 +8,7 @@ import IssuableApp from '~/issue_show/components/app.vue';
import DescriptionComponent from '~/issue_show/components/description.vue';
import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
-import { IssuableStatus, IssuableStatusText } from '~/issue_show/constants';
+import { IssuableStatus, IssuableStatusText, POLLING_DELAY } from '~/issue_show/constants';
import eventHub from '~/issue_show/event_hub';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -643,4 +643,40 @@ describe('Issuable output', () => {
});
});
});
+
+ describe('taskListUpdateStarted', () => {
+ it('stops polling', () => {
+ jest.spyOn(wrapper.vm.poll, 'stop');
+
+ wrapper.vm.taskListUpdateStarted();
+
+ expect(wrapper.vm.poll.stop).toHaveBeenCalled();
+ });
+ });
+
+ describe('taskListUpdateSucceeded', () => {
+ it('enables polling', () => {
+ jest.spyOn(wrapper.vm.poll, 'enable');
+ jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
+
+ wrapper.vm.taskListUpdateSucceeded();
+
+ expect(wrapper.vm.poll.enable).toHaveBeenCalled();
+ expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
+ });
+ });
+
+ describe('taskListUpdateFailed', () => {
+ it('enables polling and calls updateStoreState', () => {
+ jest.spyOn(wrapper.vm.poll, 'enable');
+ jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
+ jest.spyOn(wrapper.vm, 'updateStoreState');
+
+ wrapper.vm.taskListUpdateFailed();
+
+ expect(wrapper.vm.poll.enable).toHaveBeenCalled();
+ expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
+ expect(wrapper.vm.updateStoreState).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js
index cdf06ecc31f..bdcc82cab81 100644
--- a/spec/frontend/issue_show/components/description_spec.js
+++ b/spec/frontend/issue_show/components/description_spec.js
@@ -114,6 +114,8 @@ describe('Description component', () => {
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
+ onUpdate: expect.any(Function),
+ onSuccess: expect.any(Function),
onError: expect.any(Function),
lockVersion: 0,
});
@@ -150,6 +152,26 @@ describe('Description component', () => {
});
});
+ describe('taskListUpdateStarted', () => {
+ it('emits event to parent', () => {
+ const spy = jest.spyOn(vm, '$emit');
+
+ vm.taskListUpdateStarted();
+
+ expect(spy).toHaveBeenCalledWith('taskListUpdateStarted');
+ });
+ });
+
+ describe('taskListUpdateSuccess', () => {
+ it('emits event to parent', () => {
+ const spy = jest.spyOn(vm, '$emit');
+
+ vm.taskListUpdateSuccess();
+
+ expect(spy).toHaveBeenCalledWith('taskListUpdateSucceeded');
+ });
+ });
+
describe('taskListUpdateError', () => {
it('should create flash notification and emit an event to parent', () => {
const msg =
diff --git a/spec/frontend/issue_show/components/fields/type_spec.js b/spec/frontend/issue_show/components/fields/type_spec.js
index fac745716d7..95ae6f37877 100644
--- a/spec/frontend/issue_show/components/fields/type_spec.js
+++ b/spec/frontend/issue_show/components/fields/type_spec.js
@@ -39,7 +39,7 @@ describe('Issue type field component', () => {
const findTypeFromDropDownItemIconAt = (at) =>
findTypeFromDropDownItems().at(at).findComponent(GlIcon);
- const createComponent = ({ data } = {}) => {
+ const createComponent = ({ data } = {}, provide) => {
fakeApollo = createMockApollo([], mockResolvers);
wrapper = shallowMount(IssueTypeField, {
@@ -51,6 +51,10 @@ describe('Issue type field component', () => {
...data,
};
},
+ provide: {
+ canCreateIncident: true,
+ ...provide,
+ },
});
};
@@ -92,5 +96,25 @@ describe('Issue type field component', () => {
await wrapper.vm.$nextTick();
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
});
+
+ describe('when user is a guest', () => {
+ it('hides the incident type from the dropdown', async () => {
+ createComponent({}, { canCreateIncident: false, issueType: 'issue' });
+ await waitForPromises();
+
+ expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
+ expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
+ expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ });
+
+ it('and incident is selected, includes incident in the dropdown', async () => {
+ createComponent({}, { canCreateIncident: false, issueType: 'incident' });
+ await waitForPromises();
+
+ expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
+ expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
+ expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ });
+ });
});
});
diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js
index 6b443062f12..3f52c7b4afe 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -37,6 +37,7 @@ import {
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
urlSortParams,
@@ -581,6 +582,7 @@ describe('IssuesListApp component', () => {
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_TYPE },
+ { type: TOKEN_TYPE_RELEASE },
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_ITERATION },
diff --git a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
index 1fcaa99cf5a..1c9a87e8af2 100644
--- a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
+++ b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
@@ -8,7 +8,7 @@ import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import {
emptySearchProjectsQueryResponse,
project1,
- project2,
+ project3,
searchProjectsQueryResponse,
} from '../mock_data';
@@ -72,7 +72,7 @@ describe('NewIssueDropdown component', () => {
expect(inputSpy).toHaveBeenCalledTimes(1);
});
- it('renders expected dropdown items', async () => {
+ it('renders projects with issues enabled', async () => {
wrapper = mountComponent({ mountFn: mount });
await showDropdown();
@@ -80,7 +80,7 @@ describe('NewIssueDropdown component', () => {
const listItems = wrapper.findAll('li');
expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
- expect(listItems.at(1).text()).toBe(project2.nameWithNamespace);
+ expect(listItems.at(1).text()).toBe(project3.nameWithNamespace);
});
it('renders `No matches found` when there are no matches', async () => {
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index 3be256d8094..19a8af4d9c2 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -95,16 +95,29 @@ export const locationSearch = [
'assignee_username[]=lisa',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
+ 'milestone_title=season+3',
'milestone_title=season+4',
'not[milestone_title]=season+20',
+ 'not[milestone_title]=season+30',
'label_name[]=cartoon',
'label_name[]=tv',
'not[label_name][]=live action',
'not[label_name][]=drama',
+ 'release_tag=v3',
+ 'release_tag=v4',
+ 'not[release_tag]=v20',
+ 'not[release_tag]=v30',
+ 'type[]=issue',
+ 'type[]=feature',
+ 'not[type][]=bug',
+ 'not[type][]=incident',
'my_reaction_emoji=thumbsup',
- 'confidential=no',
+ 'not[my_reaction_emoji]=thumbsdown',
+ 'confidential=yes',
'iteration_id=4',
+ 'iteration_id=12',
'not[iteration_id]=20',
+ 'not[iteration_id]=42',
'epic_id=12',
'not[epic_id]=34',
'weight=1',
@@ -114,10 +127,10 @@ export const locationSearch = [
export const locationSearchWithSpecialValues = [
'assignee_id=123',
'assignee_username=bart',
- 'type[]=issue',
- 'type[]=incident',
'my_reaction_emoji=None',
'iteration_id=Current',
+ 'label_name[]=None',
+ 'release_tag=None',
'milestone_title=Upcoming',
'epic_id=None',
'weight=None',
@@ -130,16 +143,29 @@ export const filteredTokens = [
{ type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
+ { type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
+ { type: 'milestone', value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
{ type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } },
{ type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } },
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
+ { type: 'release', value: { data: 'v3', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'v4', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'v20', operator: OPERATOR_IS_NOT } },
+ { type: 'release', value: { data: 'v30', operator: OPERATOR_IS_NOT } },
+ { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } },
+ { type: 'type', value: { data: 'feature', operator: OPERATOR_IS } },
+ { type: 'type', value: { data: 'bug', operator: OPERATOR_IS_NOT } },
+ { type: 'type', value: { data: 'incident', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
- { type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
+ { type: 'my_reaction_emoji', value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
+ { type: 'confidential', value: { data: 'yes', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: '12', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
+ { type: 'iteration', value: { data: '42', operator: OPERATOR_IS_NOT } },
{ type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
@@ -151,10 +177,10 @@ export const filteredTokens = [
export const filteredTokensWithSpecialValues = [
{ type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'incident', operator: OPERATOR_IS } },
{ type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } },
+ { type: 'labels', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'Upcoming', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: 'None', operator: OPERATOR_IS } },
@@ -163,19 +189,24 @@ export const filteredTokensWithSpecialValues = [
export const apiParams = {
authorUsername: 'homer',
assigneeUsernames: ['bart', 'lisa'],
- milestoneTitle: 'season 4',
+ milestoneTitle: ['season 3', 'season 4'],
labelName: ['cartoon', 'tv'],
+ releaseTag: ['v3', 'v4'],
+ types: ['ISSUE', 'FEATURE'],
myReactionEmoji: 'thumbsup',
- confidential: 'no',
- iterationId: '4',
+ confidential: true,
+ iterationId: ['4', '12'],
epicId: '12',
weight: '1',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
- milestoneTitle: 'season 20',
+ milestoneTitle: ['season 20', 'season 30'],
labelName: ['live action', 'drama'],
- iterationId: '20',
+ releaseTag: ['v20', 'v30'],
+ types: ['BUG', 'INCIDENT'],
+ myReactionEmoji: 'thumbsdown',
+ iterationId: ['20', '42'],
epicId: '34',
weight: '3',
},
@@ -184,8 +215,9 @@ export const apiParams = {
export const apiParamsWithSpecialValues = {
assigneeId: '123',
assigneeUsernames: 'bart',
- types: ['ISSUE', 'INCIDENT'],
+ labelName: 'None',
myReactionEmoji: 'None',
+ releaseTagWildcardId: 'NONE',
iterationWildcardId: 'CURRENT',
milestoneWildcardId: 'UPCOMING',
epicId: 'None',
@@ -197,14 +229,19 @@ export const urlParams = {
'not[author_username]': 'marge',
'assignee_username[]': ['bart', 'lisa'],
'not[assignee_username][]': ['patty', 'selma'],
- milestone_title: 'season 4',
- 'not[milestone_title]': 'season 20',
+ milestone_title: ['season 3', 'season 4'],
+ 'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
+ release_tag: ['v3', 'v4'],
+ 'not[release_tag]': ['v20', 'v30'],
+ 'type[]': ['issue', 'feature'],
+ 'not[type][]': ['bug', 'incident'],
my_reaction_emoji: 'thumbsup',
- confidential: 'no',
- iteration_id: '4',
- 'not[iteration_id]': '20',
+ 'not[my_reaction_emoji]': 'thumbsdown',
+ confidential: 'yes',
+ iteration_id: ['4', '12'],
+ 'not[iteration_id]': ['20', '42'],
epic_id: '12',
'not[epic_id]': '34',
weight: '1',
@@ -214,7 +251,8 @@ export const urlParams = {
export const urlParamsWithSpecialValues = {
assignee_id: '123',
'assignee_username[]': 'bart',
- 'type[]': ['issue', 'incident'],
+ 'label_name[]': 'None',
+ release_tag: 'None',
my_reaction_emoji: 'None',
iteration_id: 'Current',
milestone_title: 'Upcoming',
@@ -224,6 +262,7 @@ export const urlParamsWithSpecialValues = {
export const project1 = {
id: 'gid://gitlab/Group/26',
+ issuesEnabled: true,
name: 'Super Mario Project',
nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
@@ -231,16 +270,25 @@ export const project1 = {
export const project2 = {
id: 'gid://gitlab/Group/59',
+ issuesEnabled: false,
name: 'Mario Kart Project',
nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
};
+export const project3 = {
+ id: 'gid://gitlab/Group/103',
+ issuesEnabled: true,
+ name: 'Mario Party Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
+};
+
export const searchProjectsQueryResponse = {
data: {
group: {
projects: {
- nodes: [project1, project2],
+ nodes: [project1, project2, project3],
},
},
},
diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues_list/utils_spec.js
index 458776d9ec5..8e1d70db92d 100644
--- a/spec/frontend/issues_list/utils_spec.js
+++ b/spec/frontend/issues_list/utils_spec.js
@@ -58,10 +58,10 @@ describe('getDueDateValue', () => {
describe('getSortOptions', () => {
describe.each`
hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
- ${false} | ${false} | ${8} | ${false} | ${false}
- ${true} | ${false} | ${9} | ${true} | ${false}
- ${false} | ${true} | ${9} | ${false} | ${true}
- ${true} | ${true} | ${10} | ${true} | ${true}
+ ${false} | ${false} | ${9} | ${false} | ${false}
+ ${true} | ${false} | ${10} | ${true} | ${false}
+ ${false} | ${true} | ${10} | ${false} | ${true}
+ ${true} | ${true} | ${11} | ${true} | ${true}
`(
'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
({
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
new file mode 100644
index 00000000000..5ec1b7b7932
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
@@ -0,0 +1,44 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
+import AddNamespaceModal from '~/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue';
+import { ADD_NAMESPACE_MODAL_ID } from '~/jira_connect/subscriptions/constants';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+describe('AddNamespaceButton', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(AddNamespaceButton, {
+ directives: {
+ glModal: createMockDirective(),
+ },
+ });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(AddNamespaceModal);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a button', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+
+ it('contains a modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('button is bound to the modal', () => {
+ const { value } = getBinding(findButton().element, 'gl-modal');
+
+ expect(value).toBeTruthy();
+ expect(value).toBe(ADD_NAMESPACE_MODAL_ID);
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
new file mode 100644
index 00000000000..d80381107f2
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import AddNamespaceModal from '~/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue';
+import GroupsList from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue';
+import { ADD_NAMESPACE_MODAL_ID } from '~/jira_connect/subscriptions/constants';
+
+describe('AddNamespaceModal', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(AddNamespaceModal);
+ };
+
+ const findModal = () => wrapper.findComponent(AddNamespaceModal);
+ const findGroupsList = () => wrapper.findComponent(GroupsList);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays modal with correct props', () => {
+ const modal = findModal();
+ expect(modal.exists()).toBe(true);
+ expect(modal.attributes()).toMatchObject({
+ modalid: ADD_NAMESPACE_MODAL_ID,
+ title: AddNamespaceModal.modal.title,
+ });
+ });
+
+ it('displays GroupList', () => {
+ expect(findGroupsList().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
index b69435df83a..15e9a740c83 100644
--- a/spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
@@ -4,9 +4,9 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
-import GroupsListItem from '~/jira_connect/subscriptions/components/groups_list_item.vue';
+import GroupsListItem from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue';
import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
-import { mockGroup1 } from '../mock_data';
+import { mockGroup1 } from '../../mock_data';
jest.mock('~/jira_connect/subscriptions/utils');
diff --git a/spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
index d3a9a3bfd41..04aba8bda23 100644
--- a/spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
@@ -3,10 +3,10 @@ import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/subscriptions/api';
-import GroupsList from '~/jira_connect/subscriptions/components/groups_list.vue';
-import GroupsListItem from '~/jira_connect/subscriptions/components/groups_list_item.vue';
+import GroupsList from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue';
+import GroupsListItem from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue';
import { DEFAULT_GROUPS_PER_PAGE } from '~/jira_connect/subscriptions/constants';
-import { mockGroup1, mockGroup2 } from '../mock_data';
+import { mockGroup1, mockGroup2 } from '../../mock_data';
const createMockGroup = (groupId) => {
return {
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 8915a7697a5..8e464968453 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -1,14 +1,17 @@
-import { GlAlert, GlButton, GlModal, GlLink } from '@gitlab/ui';
+import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
+import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
+import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { __ } from '~/locale';
+import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }),
- getLocation: jest.fn(),
}));
describe('JiraConnectApp', () => {
@@ -17,8 +20,10 @@ describe('JiraConnectApp', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findAlertLink = () => findAlert().findComponent(GlLink);
- const findGlButton = () => wrapper.findComponent(GlButton);
- const findGlModal = () => wrapper.findComponent(GlModal);
+ const findSignInButton = () => wrapper.findComponent(SignInButton);
+ const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
+ const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ provide, mountFn = shallowMount } = {}) => {
store = createStore();
@@ -34,96 +39,115 @@ describe('JiraConnectApp', () => {
});
describe('template', () => {
- describe('when user is not logged in', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- usersPath: '/users',
- },
+ describe.each`
+ scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList
+ ${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true}
+ ${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false}
+ ${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true}
+ ${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false}
+ `(
+ 'when $scenario',
+ ({
+ usersPath,
+ expectSignInButton,
+ subscriptions,
+ expectEmptyState,
+ expectNamespaceButton,
+ expectSubscriptionsList,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ usersPath,
+ subscriptions,
+ },
+ });
});
- });
- it('renders "Sign in" button', () => {
- expect(findGlButton().text()).toBe('Sign in to add namespaces');
- expect(findGlModal().exists()).toBe(false);
- });
- });
+ it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => {
+ expect(findSignInButton().exists()).toBe(expectSignInButton);
+ });
- describe('when user is logged in', () => {
- beforeEach(() => {
- createComponent();
- });
+ it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
+ expect(findEmptyState().exists()).toBe(expectEmptyState);
+ });
- it('renders "Add" button and modal', () => {
- expect(findGlButton().text()).toBe('Add namespace');
- expect(findGlModal().exists()).toBe(true);
- });
- });
+ it(`${
+ expectNamespaceButton ? 'renders' : 'does not render'
+ } button to add namespace`, () => {
+ expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
+ });
+
+ it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
+ expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
+ });
+ },
+ );
+ });
- describe('alert', () => {
- it.each`
- message | variant | alertShouldRender
- ${'Test error'} | ${'danger'} | ${true}
- ${'Test notice'} | ${'info'} | ${true}
- ${''} | ${undefined} | ${false}
- ${undefined} | ${undefined} | ${false}
- `(
- 'renders correct alert when message is `$message` and variant is `$variant`',
- async ({ message, alertShouldRender, variant }) => {
- createComponent();
-
- store.commit(SET_ALERT, { message, variant });
- await wrapper.vm.$nextTick();
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(alertShouldRender);
- if (alertShouldRender) {
- expect(alert.isVisible()).toBe(alertShouldRender);
- expect(alert.html()).toContain(message);
- expect(alert.props('variant')).toBe(variant);
- expect(findAlertLink().exists()).toBe(false);
- }
- },
- );
-
- it('hides alert on @dismiss event', async () => {
+ describe('alert', () => {
+ it.each`
+ message | variant | alertShouldRender
+ ${'Test error'} | ${'danger'} | ${true}
+ ${'Test notice'} | ${'info'} | ${true}
+ ${''} | ${undefined} | ${false}
+ ${undefined} | ${undefined} | ${false}
+ `(
+ 'renders correct alert when message is `$message` and variant is `$variant`',
+ async ({ message, alertShouldRender, variant }) => {
createComponent();
- store.commit(SET_ALERT, { message: 'test message' });
+ store.commit(SET_ALERT, { message, variant });
await wrapper.vm.$nextTick();
- findAlert().vm.$emit('dismiss');
- await wrapper.vm.$nextTick();
+ const alert = findAlert();
- expect(findAlert().exists()).toBe(false);
- });
+ expect(alert.exists()).toBe(alertShouldRender);
+ if (alertShouldRender) {
+ expect(alert.isVisible()).toBe(alertShouldRender);
+ expect(alert.html()).toContain(message);
+ expect(alert.props('variant')).toBe(variant);
+ expect(findAlertLink().exists()).toBe(false);
+ }
+ },
+ );
- it('renders link when `linkUrl` is set', async () => {
- createComponent({ mountFn: mount });
+ it('hides alert on @dismiss event', async () => {
+ createComponent();
- store.commit(SET_ALERT, {
- message: __('test message %{linkStart}test link%{linkEnd}'),
- linkUrl: 'https://gitlab.com',
- });
- await wrapper.vm.$nextTick();
+ store.commit(SET_ALERT, { message: 'test message' });
+ await wrapper.vm.$nextTick();
+
+ findAlert().vm.$emit('dismiss');
+ await wrapper.vm.$nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
- const alertLink = findAlertLink();
+ it('renders link when `linkUrl` is set', async () => {
+ createComponent({ mountFn: mount });
- expect(alertLink.exists()).toBe(true);
- expect(alertLink.text()).toContain('test link');
- expect(alertLink.attributes('href')).toBe('https://gitlab.com');
+ store.commit(SET_ALERT, {
+ message: __('test message %{linkStart}test link%{linkEnd}'),
+ linkUrl: 'https://gitlab.com',
});
+ await wrapper.vm.$nextTick();
- describe('when alert is set in localStoage', () => {
- it('renders alert on mount', () => {
- createComponent();
+ const alertLink = findAlertLink();
- const alert = findAlert();
+ expect(alertLink.exists()).toBe(true);
+ expect(alertLink.text()).toContain('test link');
+ expect(alertLink.attributes('href')).toBe('https://gitlab.com');
+ });
- expect(alert.exists()).toBe(true);
- expect(alert.html()).toContain('error message');
- });
+ describe('when alert is set in localStoage', () => {
+ it('renders alert on mount', () => {
+ createComponent();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.html()).toContain('error message');
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
new file mode 100644
index 00000000000..cb5ae877c47
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
@@ -0,0 +1,48 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
+import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+const MOCK_USERS_PATH = '/user';
+
+jest.mock('~/jira_connect/subscriptions/utils');
+
+describe('SignInButton', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(SignInButton, {
+ propsData: {
+ usersPath: MOCK_USERS_PATH,
+ },
+ });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a button', () => {
+ createComponent();
+
+ expect(findButton().exists()).toBe(true);
+ });
+
+ describe.each`
+ expectedHref
+ ${MOCK_USERS_PATH}
+ ${`${MOCK_USERS_PATH}?return_to=${encodeURIComponent('https://test.jira.com')}`}
+ `('when getGitlabSignInURL resolves with `$expectedHref`', ({ expectedHref }) => {
+ it(`sets button href to ${expectedHref}`, async () => {
+ getGitlabSignInURL.mockResolvedValue(expectedHref);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findButton().attributes('href')).toBe(expectedHref);
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
index 32b43765843..4e4a2b58600 100644
--- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
@@ -1,12 +1,15 @@
-import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
+import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
+
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { reloadPage } from '~/jira_connect/subscriptions/utils';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils');
@@ -15,11 +18,13 @@ describe('SubscriptionsList', () => {
let wrapper;
let store;
- const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
+ const createComponent = () => {
store = createStore();
- wrapper = mountFn(SubscriptionsList, {
- provide,
+ wrapper = mount(SubscriptionsList, {
+ provide: {
+ subscriptions: [mockSubscription],
+ },
store,
});
};
@@ -28,28 +33,28 @@ describe('SubscriptionsList', () => {
wrapper.destroy();
});
- const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findGlTable = () => wrapper.findComponent(GlTable);
- const findUnlinkButton = () => findGlTable().findComponent(GlButton);
+ const findUnlinkButton = () => wrapper.findComponent(GlButton);
const clickUnlinkButton = () => findUnlinkButton().trigger('click');
describe('template', () => {
- it('renders GlEmptyState when subscriptions is empty', () => {
+ beforeEach(() => {
createComponent();
+ });
+
+ it('renders "name" cell correctly', () => {
+ const groupItemNames = wrapper.findAllComponents(GroupItemName);
+ expect(groupItemNames.wrappers).toHaveLength(1);
- expect(findGlEmptyState().exists()).toBe(true);
- expect(findGlTable().exists()).toBe(false);
+ const item = groupItemNames.at(0);
+ expect(item.props('group')).toBe(mockSubscription.group);
});
- it('renders GlTable when subscriptions are present', () => {
- createComponent({
- provide: {
- subscriptions: [mockSubscription],
- },
- });
+ it('renders "created at" cell correctly', () => {
+ const timeAgoTooltips = wrapper.findAllComponents(TimeagoTooltip);
+ expect(timeAgoTooltips.wrappers).toHaveLength(1);
- expect(findGlEmptyState().exists()).toBe(false);
- expect(findGlTable().exists()).toBe(true);
+ const item = timeAgoTooltips.at(0);
+ expect(item.props('time')).toBe(mockSubscription.created_at);
});
});
@@ -57,12 +62,7 @@ describe('SubscriptionsList', () => {
let removeSubscriptionSpy;
beforeEach(() => {
- createComponent({
- mountFn: mount,
- provide: {
- subscriptions: [mockSubscription],
- },
- });
+ createComponent();
removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue();
});
diff --git a/spec/frontend/jira_connect/subscriptions/index_spec.js b/spec/frontend/jira_connect/subscriptions/index_spec.js
index 786f3b4a7d3..b97918a198e 100644
--- a/spec/frontend/jira_connect/subscriptions/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/index_spec.js
@@ -1,24 +1,36 @@
import { initJiraConnect } from '~/jira_connect/subscriptions';
+import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
-jest.mock('~/jira_connect/subscriptions/utils', () => ({
- getLocation: jest.fn().mockResolvedValue('test/location'),
-}));
+jest.mock('~/jira_connect/subscriptions/utils');
describe('initJiraConnect', () => {
- beforeEach(async () => {
+ const mockInitialHref = 'https://gitlab.com';
+
+ beforeEach(() => {
setFixtures(`
- <a class="js-jira-connect-sign-in" href="https://gitlab.com">Sign In</a>
- <a class="js-jira-connect-sign-in" href="https://gitlab.com">Another Sign In</a>
+ <a class="js-jira-connect-sign-in" href="${mockInitialHref}">Sign In</a>
+ <a class="js-jira-connect-sign-in" href="${mockInitialHref}">Another Sign In</a>
`);
-
- await initJiraConnect();
});
+ const assertSignInLinks = (expectedLink) => {
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
+ expect(el.getAttribute('href')).toBe(expectedLink);
+ });
+ };
+
describe('Sign in links', () => {
- it('have `return_to` query parameter', () => {
- Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
- expect(el.href).toContain('return_to=test/location');
- });
+ it('are updated on initialization', async () => {
+ const mockSignInLink = `https://gitlab.com?return_to=${encodeURIComponent('/test/location')}`;
+ getGitlabSignInURL.mockResolvedValue(mockSignInLink);
+
+ // assert the initial state
+ assertSignInLinks(mockInitialHref);
+
+ await initJiraConnect();
+
+ // assert the update has occurred
+ assertSignInLinks(mockSignInLink);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/utils_spec.js b/spec/frontend/jira_connect/subscriptions/utils_spec.js
index 2dd95de1b8c..762d9eb3443 100644
--- a/spec/frontend/jira_connect/subscriptions/utils_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/utils_spec.js
@@ -8,6 +8,7 @@ import {
getLocation,
reloadPage,
sizeToParent,
+ getGitlabSignInURL,
} from '~/jira_connect/subscriptions/utils';
describe('JiraConnect utils', () => {
@@ -137,4 +138,25 @@ describe('JiraConnect utils', () => {
});
});
});
+
+ describe('getGitlabSignInURL', () => {
+ const mockSignInURL = 'https://gitlab.com/sign_in';
+
+ it.each`
+ returnTo | expectResult
+ ${undefined} | ${mockSignInURL}
+ ${''} | ${mockSignInURL}
+ ${'/test/location'} | ${`${mockSignInURL}?return_to=${encodeURIComponent('/test/location')}`}
+ `(
+ 'returns `$expectResult` when `AP.getLocation` resolves to `$returnTo`',
+ async ({ returnTo, expectResult }) => {
+ global.AP = {
+ getLocation: jest.fn().mockImplementation((cb) => cb(returnTo)),
+ };
+
+ const url = await getGitlabSignInURL(mockSignInURL);
+ expect(url).toBe(expectResult);
+ },
+ );
+ });
});
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index 7e42ee957d3..a5278af8e33 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
@@ -1,9 +1,9 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { createLocalVue, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Form from '~/jobs/components/manual_variables_form.vue';
+import ManualVariablesForm from '~/jobs/components/manual_variables_form.vue';
const localVue = createLocalVue();
@@ -21,7 +21,7 @@ describe('Manual Variables Form', () => {
},
};
- const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = (props = {}) => {
store = new Vuex.Store({
actions: {
triggerManualJob: jest.fn(),
@@ -29,7 +29,7 @@ describe('Manual Variables Form', () => {
});
wrapper = extendedWrapper(
- mountFn(localVue.extend(Form), {
+ mount(localVue.extend(ManualVariablesForm), {
propsData: { ...requiredProps, ...props },
localVue,
store,
@@ -40,88 +40,120 @@ describe('Manual Variables Form', () => {
);
};
- const findInputKey = () => wrapper.findComponent({ ref: 'inputKey' });
- const findInputValue = () => wrapper.findComponent({ ref: 'inputSecretValue' });
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
+ const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
+ const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
+ const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
+ const setCiVariableKey = () => {
+ findCiVariableKey().setValue('new key');
+ findCiVariableKey().vm.$emit('change');
+ nextTick();
+ };
+
+ const setCiVariableKeyByPosition = (position, value) => {
+ findAllCiVariableKeys().at(position).setValue(value);
+ findAllCiVariableKeys().at(position).vm.$emit('change');
+ nextTick();
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
afterEach(() => {
wrapper.destroy();
});
- describe('shallowMount', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('creates a new variable when user enters a new key value', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- it('renders empty form with correct placeholders', () => {
- expect(findInputKey().attributes('placeholder')).toBe('Input variable key');
- expect(findInputValue().attributes('placeholder')).toBe('Input variable value');
- });
+ await setCiVariableKey();
- it('renders help text with provided link', () => {
- expect(findHelpText().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(
- '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
- );
- });
+ expect(findAllVariables()).toHaveLength(2);
+ });
- describe('when adding a new variable', () => {
- it('creates a new variable when user types a new key and resets the form', async () => {
- await findInputKey().setValue('new key');
+ it('does not create extra empty variables', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- expect(findAllVariables()).toHaveLength(1);
- expect(findCiVariableKey().element.value).toBe('new key');
- expect(findInputKey().attributes('value')).toBe(undefined);
- });
+ await setCiVariableKey();
- it('creates a new variable when user types a new value and resets the form', async () => {
- await findInputValue().setValue('new value');
+ expect(findAllVariables()).toHaveLength(2);
- expect(findAllVariables()).toHaveLength(1);
- expect(findCiVariableValue().element.value).toBe('new value');
- expect(findInputValue().attributes('value')).toBe(undefined);
- });
- });
+ await setCiVariableKey();
+
+ expect(findAllVariables()).toHaveLength(2);
});
- describe('mount', () => {
- beforeEach(() => {
- createComponent({ mountFn: mount });
- });
+ it('removes the correct variable row', async () => {
+ const variableKeyNameOne = 'key-one';
+ const variableKeyNameThree = 'key-three';
- describe('when deleting a variable', () => {
- it('removes the variable row', async () => {
- await wrapper.setData({
- variables: [
- {
- key: 'new key',
- secret_value: 'value',
- id: '1',
- },
- ],
- });
+ await setCiVariableKeyByPosition(0, variableKeyNameOne);
- findDeleteVarBtn().trigger('click');
+ await setCiVariableKeyByPosition(1, 'key-two');
- await wrapper.vm.$nextTick();
+ await setCiVariableKeyByPosition(2, variableKeyNameThree);
- expect(findAllVariables()).toHaveLength(0);
- });
- });
+ expect(findAllVariables()).toHaveLength(4);
- it('trigger button is disabled after trigger action', async () => {
- expect(findTriggerBtn().props('disabled')).toBe(false);
+ await findAllDeleteVarBtns().at(1).trigger('click');
- await findTriggerBtn().trigger('click');
+ expect(findAllVariables()).toHaveLength(3);
- expect(findTriggerBtn().props('disabled')).toBe(true);
- });
+ expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
+ expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
+ expect(findAllCiVariableKeys().at(2).element.value).toBe('');
+ });
+
+ it('trigger button is disabled after trigger action', async () => {
+ expect(findTriggerBtn().props('disabled')).toBe(false);
+
+ await findTriggerBtn().trigger('click');
+
+ expect(findTriggerBtn().props('disabled')).toBe(true);
+ });
+
+ it('delete variable button should only show when there is more than one variable', async () => {
+ expect(findDeleteVarBtn().exists()).toBe(false);
+
+ await setCiVariableKey();
+
+ expect(findDeleteVarBtn().exists()).toBe(true);
+ });
+
+ it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+ expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+ });
+
+ it('renders help text with provided link', () => {
+ expect(findHelpText().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(
+ '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
+ );
+ });
+
+ it('passes variables in correct format', async () => {
+ jest.spyOn(store, 'dispatch');
+
+ await setCiVariableKey();
+
+ await findCiVariableValue().setValue('new value');
+
+ await findTriggerBtn().trigger('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
+ {
+ key: 'new key',
+ secret_value: 'new value',
+ },
+ ]);
});
});
diff --git a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
index 852106db44e..7b604724977 100644
--- a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
+++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
@@ -47,107 +47,95 @@ describe('getSuppressNetworkErrorsDuringNavigationLink', () => {
subscription = link.request(mockOperation).subscribe(observer);
};
- describe('when disabled', () => {
- it('returns null', () => {
- expect(getSuppressNetworkErrorsDuringNavigationLink()).toBe(null);
- });
+ it('returns an ApolloLink', () => {
+ expect(getSuppressNetworkErrorsDuringNavigationLink()).toEqual(expect.any(ApolloLink));
});
- describe('when enabled', () => {
- beforeEach(() => {
- window.gon = { features: { suppressApolloErrorsDuringNavigation: true } };
- });
-
- it('returns an ApolloLink', () => {
- expect(getSuppressNetworkErrorsDuringNavigationLink()).toEqual(expect.any(ApolloLink));
- });
-
- describe('suppression case', () => {
- describe('when navigating away', () => {
- beforeEach(() => {
- isNavigatingAway.mockReturnValue(true);
- });
-
- describe('given a network error', () => {
- it('does not forward the error', async () => {
- const spy = jest.fn();
+ describe('suppression case', () => {
+ describe('when navigating away', () => {
+ beforeEach(() => {
+ isNavigatingAway.mockReturnValue(true);
+ });
- createSubscription(makeMockNetworkErrorLink(), {
- next: spy,
- error: spy,
- complete: spy,
- });
+ describe('given a network error', () => {
+ it('does not forward the error', async () => {
+ const spy = jest.fn();
- // It's hard to test for something _not_ happening. The best we can
- // do is wait a bit to make sure nothing happens.
- await waitForPromises();
- expect(spy).not.toHaveBeenCalled();
+ createSubscription(makeMockNetworkErrorLink(), {
+ next: spy,
+ error: spy,
+ complete: spy,
});
+
+ // It's hard to test for something _not_ happening. The best we can
+ // do is wait a bit to make sure nothing happens.
+ await waitForPromises();
+ expect(spy).not.toHaveBeenCalled();
});
});
});
+ });
- describe('non-suppression cases', () => {
- describe('when not navigating away', () => {
- beforeEach(() => {
- isNavigatingAway.mockReturnValue(false);
- });
+ describe('non-suppression cases', () => {
+ describe('when not navigating away', () => {
+ beforeEach(() => {
+ isNavigatingAway.mockReturnValue(false);
+ });
- it('forwards successful requests', (done) => {
- createSubscription(makeMockSuccessLink(), {
- next({ data }) {
- expect(data).toEqual({ foo: { id: 1 } });
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards successful requests', (done) => {
+ createSubscription(makeMockSuccessLink(), {
+ next({ data }) {
+ expect(data).toEqual({ foo: { id: 1 } });
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
+ });
- it('forwards GraphQL errors', (done) => {
- createSubscription(makeMockGraphQLErrorLink(), {
- next({ errors }) {
- expect(errors).toEqual([{ message: 'foo' }]);
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards GraphQL errors', (done) => {
+ createSubscription(makeMockGraphQLErrorLink(), {
+ next({ errors }) {
+ expect(errors).toEqual([{ message: 'foo' }]);
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
+ });
- it('forwards network errors', (done) => {
- createSubscription(makeMockNetworkErrorLink(), {
- next: () => done.fail('Should not happen'),
- error: (error) => {
- expect(error.message).toBe('NetworkError');
- done();
- },
- complete: () => done.fail('Should not happen'),
- });
+ it('forwards network errors', (done) => {
+ createSubscription(makeMockNetworkErrorLink(), {
+ next: () => done.fail('Should not happen'),
+ error: (error) => {
+ expect(error.message).toBe('NetworkError');
+ done();
+ },
+ complete: () => done.fail('Should not happen'),
});
});
+ });
- describe('when navigating away', () => {
- beforeEach(() => {
- isNavigatingAway.mockReturnValue(true);
- });
+ describe('when navigating away', () => {
+ beforeEach(() => {
+ isNavigatingAway.mockReturnValue(true);
+ });
- it('forwards successful requests', (done) => {
- createSubscription(makeMockSuccessLink(), {
- next({ data }) {
- expect(data).toEqual({ foo: { id: 1 } });
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards successful requests', (done) => {
+ createSubscription(makeMockSuccessLink(), {
+ next({ data }) {
+ expect(data).toEqual({ foo: { id: 1 } });
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
+ });
- it('forwards GraphQL errors', (done) => {
- createSubscription(makeMockGraphQLErrorLink(), {
- next({ errors }) {
- expect(errors).toEqual([{ message: 'foo' }]);
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards GraphQL errors', (done) => {
+ createSubscription(makeMockGraphQLErrorLink(), {
+ next({ errors }) {
+ expect(errors).toEqual([{ message: 'foo' }]);
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index f5a74ee7f09..de1be5bc337 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -279,6 +279,14 @@ describe('common_utils', () => {
top: elementTopWithContext,
});
});
+
+ it('passes through behaviour', () => {
+ commonUtils.scrollToElementWithContext(`#${id}`, { behavior: 'smooth' });
+ expect(window.scrollTo).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ top: elementTopWithContext,
+ });
+ });
});
});
@@ -1000,6 +1008,21 @@ describe('common_utils', () => {
});
});
+ describe('scopedLabelKey', () => {
+ it.each`
+ label | expectedLabelKey
+ ${undefined} | ${''}
+ ${''} | ${''}
+ ${'title'} | ${'title'}
+ ${'scoped::value'} | ${'scoped'}
+ ${'scoped::label::value'} | ${'scoped::label'}
+ ${'scoped::label-some::value'} | ${'scoped::label-some'}
+ ${'scoped::label::some::value'} | ${'scoped::label::some'}
+ `('returns "$expectedLabelKey" when label is "$label"', ({ label, expectedLabelKey }) => {
+ expect(commonUtils.scopedLabelKey({ title: label })).toBe(expectedLabelKey);
+ });
+ });
+
describe('getDashPath', () => {
it('returns the path following /-/', () => {
expect(commonUtils.getDashPath('/some/-/url-with-dashes-/')).toEqual('url-with-dashes-/');
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
new file mode 100644
index 00000000000..d19f9352bbc
--- /dev/null
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -0,0 +1,59 @@
+import { GlModal } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
+
+describe('Confirm Modal', () => {
+ let wrapper;
+ let modal;
+
+ const createComponent = ({ primaryText, primaryVariant } = {}) => {
+ wrapper = mount(ConfirmModal, {
+ propsData: {
+ primaryText,
+ primaryVariant,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlModal = () => wrapper.findComponent(GlModal);
+
+ describe('Modal events', () => {
+ beforeEach(() => {
+ createComponent();
+ modal = findGlModal();
+ });
+
+ it('should emit `confirmed` event on `primary` modal event', () => {
+ findGlModal().vm.$emit('primary');
+ expect(wrapper.emitted('confirmed')).toBeTruthy();
+ });
+
+ it('should emit closed` event on `hidden` modal event', () => {
+ modal.vm.$emit('hidden');
+ expect(wrapper.emitted('closed')).toBeTruthy();
+ });
+ });
+
+ describe('Custom properties', () => {
+ it('should pass correct custom primary text & button variant to the modal when provided', () => {
+ const primaryText = "Let's do it!";
+ const primaryVariant = 'danger';
+
+ createComponent({ primaryText, primaryVariant });
+ const customProps = findGlModal().props('actionPrimary');
+ expect(customProps.text).toBe(primaryText);
+ expect(customProps.attributes.variant).toBe(primaryVariant);
+ });
+
+ it('should pass default primary text & button variant to the modal if no custom values provided', () => {
+ createComponent();
+ const customProps = findGlModal().props('actionPrimary');
+ expect(customProps.text).toBe('OK');
+ expect(customProps.attributes.variant).toBe('confirm');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index f6ad41d5478..7a64b654baa 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -185,15 +185,15 @@ describe('dateInWords', () => {
const date = new Date('07/01/2016');
it('should return date in words', () => {
- expect(datetimeUtility.dateInWords(date)).toEqual(s__('July 1, 2016'));
+ expect(datetimeUtility.dateInWords(date)).toEqual(__('July 1, 2016'));
});
it('should return abbreviated month name', () => {
- expect(datetimeUtility.dateInWords(date, true)).toEqual(s__('Jul 1, 2016'));
+ expect(datetimeUtility.dateInWords(date, true)).toEqual(__('Jul 1, 2016'));
});
it('should return date in words without year', () => {
- expect(datetimeUtility.dateInWords(date, true, true)).toEqual(s__('Jul 1'));
+ expect(datetimeUtility.dateInWords(date, true, true)).toEqual(__('Jul 1'));
});
});
@@ -201,11 +201,11 @@ describe('monthInWords', () => {
const date = new Date('2017-01-20');
it('returns month name from provided date', () => {
- expect(datetimeUtility.monthInWords(date)).toBe(s__('January'));
+ expect(datetimeUtility.monthInWords(date)).toBe(__('January'));
});
it('returns abbreviated month name from provided date', () => {
- expect(datetimeUtility.monthInWords(date, true)).toBe(s__('Jan'));
+ expect(datetimeUtility.monthInWords(date, true)).toBe(__('Jan'));
});
});
diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index 1dff5d4f925..ff11107ea60 100644
--- a/spec/frontend/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -1,4 +1,4 @@
-import fileUpload, { getFilename } from '~/lib/utils/file_upload';
+import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
@@ -64,13 +64,23 @@ describe('File upload', () => {
});
describe('getFilename', () => {
- it('returns first value correctly', () => {
- const event = {
- clipboardData: {
- getData: () => 'test.png\rtest.txt',
- },
- };
-
- expect(getFilename(event)).toBe('test.png');
+ it('returns file name', () => {
+ const file = new File([], 'test.jpg');
+
+ expect(getFilename(file)).toBe('test.jpg');
+ });
+});
+
+describe('file name validator', () => {
+ it('validate file name', () => {
+ const file = new File([], 'test.jpg');
+
+ expect(validateImageName(file)).toBe('test.jpg');
+ });
+
+ it('illegal file name should be rename to image.png', () => {
+ const file = new File([], 'test<.png');
+
+ expect(validateImageName(file)).toBe('image.png');
});
});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index acbf1a975b8..ab81ec47b64 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -100,11 +100,11 @@ describe('init markdown', () => {
text: textArea.value,
tag: '```suggestion:-0+0\n{text}\n```',
blockTag: true,
- selected: '# Does not parse the %br currently.',
+ selected: '# Does not %br parse the %br currently.',
wrap: false,
});
- expect(textArea.value).toContain('# Does not parse the \\n currently.');
+ expect(textArea.value).toContain('# Does not \\n parse the \\n currently.');
});
it('inserts the tag on the same line if the current line only contains spaces', () => {
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 36e1a453ef4..c6edba19c56 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1060,4 +1060,12 @@ describe('URL utility', () => {
},
);
});
+
+ describe('defaultPromoUrl', () => {
+ it('Gitlab about page url', () => {
+ const url = 'https://about.gitlab.com';
+
+ expect(urlUtils.PROMO_URL).toBe(url);
+ });
+ });
});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index f42ee295511..218db0b587a 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -39,7 +39,7 @@ export const member = {
Developer: 30,
Maintainer: 40,
Owner: 50,
- 'Minimal Access': 5,
+ 'Minimal access': 5,
},
};
diff --git a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
deleted file mode 100644
index 2a8ce1d3f30..00000000000
--- a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
+++ /dev/null
@@ -1,43 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AlertWidget Alert firing displays a warning icon and matches snapshot 1`] = `
-<gl-badge-stub
- class="d-flex-center text-truncate"
- size="md"
- variant="danger"
->
- <gl-icon-stub
- class="flex-shrink-0"
- name="warning"
- size="16"
- />
-
- <span
- class="text-truncate gl-pl-2"
- >
- Firing:
- alert-label &gt; 42
-
- </span>
-</gl-badge-stub>
-`;
-
-exports[`AlertWidget Alert not firing displays a warning icon and matches snapshot 1`] = `
-<gl-badge-stub
- class="d-flex-center text-truncate"
- size="md"
- variant="neutral"
->
- <gl-icon-stub
- class="flex-shrink-0"
- name="warning"
- size="16"
- />
-
- <span
- class="text-truncate gl-pl-2"
- >
- alert-label &gt; 42
- </span>
-</gl-badge-stub>
-`;
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
deleted file mode 100644
index 9bf9e8ad7cc..00000000000
--- a/spec/frontend/monitoring/alert_widget_spec.js
+++ /dev/null
@@ -1,423 +0,0 @@
-import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
-import AlertWidget from '~/monitoring/components/alert_widget.vue';
-
-const mockReadAlert = jest.fn();
-const mockCreateAlert = jest.fn();
-const mockUpdateAlert = jest.fn();
-const mockDeleteAlert = jest.fn();
-
-jest.mock('~/flash');
-jest.mock(
- '~/monitoring/services/alerts_service',
- () =>
- function AlertsServiceMock() {
- return {
- readAlert: mockReadAlert,
- createAlert: mockCreateAlert,
- updateAlert: mockUpdateAlert,
- deleteAlert: mockDeleteAlert,
- };
- },
-);
-
-describe('AlertWidget', () => {
- let wrapper;
-
- const nonFiringAlertResult = [
- {
- values: [
- [0, 1],
- [1, 42],
- [2, 41],
- ],
- },
- ];
- const firingAlertResult = [
- {
- values: [
- [0, 42],
- [1, 43],
- [2, 44],
- ],
- },
- ];
- const metricId = '5';
- const alertPath = 'my/alert.json';
-
- const relevantQueries = [
- {
- metricId,
- label: 'alert-label',
- alert_path: alertPath,
- result: nonFiringAlertResult,
- },
- ];
-
- const firingRelevantQueries = [
- {
- metricId,
- label: 'alert-label',
- alert_path: alertPath,
- result: firingAlertResult,
- },
- ];
-
- const defaultProps = {
- alertsEndpoint: '',
- relevantQueries,
- alertsToManage: {},
- modalId: 'alert-modal-1',
- };
-
- const propsWithAlert = {
- relevantQueries,
- };
-
- const propsWithAlertData = {
- relevantQueries,
- alertsToManage: {
- [alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId },
- },
- };
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(AlertWidget, {
- stubs: { GlTooltip, GlSprintf },
- propsData: {
- ...defaultProps,
- ...propsData,
- },
- });
- };
- const hasLoadingIcon = () => wrapper.find(GlLoadingIcon).exists();
- const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' });
- const findAlertErrorMessage = () => wrapper.find({ ref: 'alertErrorMessage' });
- const findCurrentSettingsText = () =>
- wrapper.find({ ref: 'alertCurrentSetting' }).text().replace(/\s\s+/g, ' ');
- const findBadge = () => wrapper.find(GlBadge);
- const findTooltip = () => wrapper.find(GlTooltip);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('displays a loading spinner and disables form when fetching alerts', () => {
- let resolveReadAlert;
- mockReadAlert.mockReturnValue(
- new Promise((resolve) => {
- resolveReadAlert = resolve;
- }),
- );
- createComponent(defaultProps);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(hasLoadingIcon()).toBe(true);
- expect(findWidgetForm().props('disabled')).toBe(true);
-
- resolveReadAlert({ operator: '==', threshold: 42 });
- })
- .then(() => waitForPromises())
- .then(() => {
- expect(hasLoadingIcon()).toBe(false);
- expect(findWidgetForm().props('disabled')).toBe(false);
- });
- });
-
- it('does not render loading spinner if showLoadingState is false', () => {
- let resolveReadAlert;
- mockReadAlert.mockReturnValue(
- new Promise((resolve) => {
- resolveReadAlert = resolve;
- }),
- );
- createComponent({
- ...defaultProps,
- showLoadingState: false,
- });
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
-
- resolveReadAlert({ operator: '==', threshold: 42 });
- })
- .then(() => waitForPromises())
- .then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- });
- });
-
- it('displays an error message when fetch fails', () => {
- mockReadAlert.mockRejectedValue();
- createComponent(propsWithAlert);
- expect(hasLoadingIcon()).toBe(true);
-
- return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalled();
- expect(hasLoadingIcon()).toBe(false);
- });
- });
-
- describe('Alert not firing', () => {
- it('displays a warning icon and matches snapshot', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- createComponent(propsWithAlertData);
-
- return waitForPromises().then(() => {
- expect(findBadge().element).toMatchSnapshot();
- });
- });
-
- it('displays an alert summary when there is a single alert', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- createComponent(propsWithAlertData);
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toEqual('alert-label > 42');
- });
- });
-
- it('displays a combined alert summary when there are multiple alerts', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const propsWithManyAlerts = {
- relevantQueries: [
- ...relevantQueries,
- ...[
- {
- metricId: '6',
- alert_path: 'my/alert2.json',
- label: 'alert-label2',
- result: [{ values: [] }],
- },
- ],
- ],
- alertsToManage: {
- 'my/alert.json': {
- operator: '>',
- threshold: 42,
- alert_path: alertPath,
- metricId,
- },
- 'my/alert2.json': {
- operator: '==',
- threshold: 900,
- alert_path: 'my/alert2.json',
- metricId: '6',
- },
- },
- };
- createComponent(propsWithManyAlerts);
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toContain('2 alerts applied');
- });
- });
- });
-
- describe('Alert firing', () => {
- it('displays a warning icon and matches snapshot', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- propsWithAlertData.relevantQueries = firingRelevantQueries;
- createComponent(propsWithAlertData);
-
- return waitForPromises().then(() => {
- expect(findBadge().element).toMatchSnapshot();
- });
- });
-
- it('displays an alert summary when there is a single alert', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- propsWithAlertData.relevantQueries = firingRelevantQueries;
- createComponent(propsWithAlertData);
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toEqual('Firing: alert-label > 42');
- });
- });
-
- it('displays a combined alert summary when there are multiple alerts', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const propsWithManyAlerts = {
- relevantQueries: [
- ...firingRelevantQueries,
- ...[
- {
- metricId: '6',
- alert_path: 'my/alert2.json',
- label: 'alert-label2',
- result: [{ values: [] }],
- },
- ],
- ],
- alertsToManage: {
- 'my/alert.json': {
- operator: '>',
- threshold: 42,
- alert_path: alertPath,
- metricId,
- },
- 'my/alert2.json': {
- operator: '==',
- threshold: 900,
- alert_path: 'my/alert2.json',
- metricId: '6',
- },
- },
- };
- createComponent(propsWithManyAlerts);
-
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toContain('2 alerts applied, 1 firing');
- });
- });
-
- it('should display tooltip with thresholds summary', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const propsWithManyAlerts = {
- relevantQueries: [
- ...firingRelevantQueries,
- ...[
- {
- metricId: '6',
- alert_path: 'my/alert2.json',
- label: 'alert-label2',
- result: [{ values: [] }],
- },
- ],
- ],
- alertsToManage: {
- 'my/alert.json': {
- operator: '>',
- threshold: 42,
- alert_path: alertPath,
- metricId,
- },
- 'my/alert2.json': {
- operator: '==',
- threshold: 900,
- alert_path: 'my/alert2.json',
- metricId: '6',
- },
- },
- };
- createComponent(propsWithManyAlerts);
-
- return waitForPromises().then(() => {
- expect(findTooltip().text().replace(/\s\s+/g, ' ')).toEqual('Firing: alert-label > 42');
- });
- });
- });
-
- it('creates an alert with an appropriate handler', () => {
- const alertParams = {
- operator: '<',
- threshold: 4,
- prometheus_metric_id: '5',
- };
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const fakeAlertPath = 'foo/bar';
- mockCreateAlert.mockResolvedValue({ alert_path: fakeAlertPath, ...alertParams });
- createComponent({
- alertsToManage: {
- [fakeAlertPath]: {
- alert_path: fakeAlertPath,
- operator: '<',
- threshold: 4,
- prometheus_metric_id: '5',
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('create', alertParams);
-
- expect(mockCreateAlert).toHaveBeenCalledWith(alertParams);
- });
-
- it('updates an alert with an appropriate handler', () => {
- const alertParams = { operator: '<', threshold: 4, alert_path: alertPath };
- const newAlertParams = { operator: '==', threshold: 12 };
- mockReadAlert.mockResolvedValue(alertParams);
- mockUpdateAlert.mockResolvedValue({ ...alertParams, ...newAlertParams });
- createComponent({
- ...propsWithAlertData,
- alertsToManage: {
- [alertPath]: {
- alert_path: alertPath,
- operator: '==',
- threshold: 12,
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('update', {
- alert: alertPath,
- ...newAlertParams,
- prometheus_metric_id: '5',
- });
-
- expect(mockUpdateAlert).toHaveBeenCalledWith(alertPath, newAlertParams);
- });
-
- it('deletes an alert with an appropriate handler', () => {
- const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
- mockReadAlert.mockResolvedValue(alertParams);
- mockDeleteAlert.mockResolvedValue({});
- createComponent({
- ...propsWithAlert,
- alertsToManage: {
- [alertPath]: {
- alert_path: alertPath,
- operator: '>',
- threshold: 42,
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('delete', { alert: alertPath });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(mockDeleteAlert).toHaveBeenCalledWith(alertPath);
- expect(findAlertErrorMessage().exists()).toBe(false);
- });
- });
-
- describe('when delete fails', () => {
- beforeEach(() => {
- const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
- mockReadAlert.mockResolvedValue(alertParams);
- mockDeleteAlert.mockRejectedValue();
-
- createComponent({
- ...propsWithAlert,
- alertsToManage: {
- [alertPath]: {
- alert_path: alertPath,
- operator: '>',
- threshold: 42,
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('delete', { alert: alertPath });
- return wrapper.vm.$nextTick();
- });
-
- it('shows error message', () => {
- expect(findAlertErrorMessage().text()).toEqual('Error deleting alert');
- });
-
- it('dismisses error message on cancel', () => {
- findWidgetForm().vm.$emit('cancel');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findAlertErrorMessage().exists()).toBe(false);
- });
- });
- });
-});
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 47b6c463377..aaa0a91ffe0 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -8,8 +8,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
metricsdashboardbasepath="/monitoring/monitor-project/-/environments/1/metrics"
metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
>
- <alerts-deprecation-warning-stub />
-
<div
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
>
diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js
deleted file mode 100644
index e0ef1040f6b..00000000000
--- a/spec/frontend/monitoring/components/alert_widget_form_spec.js
+++ /dev/null
@@ -1,242 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import INVALID_URL from '~/lib/utils/invalid_url';
-import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue';
-import ModalStub from '../stubs/modal_stub';
-
-describe('AlertWidgetForm', () => {
- let wrapper;
-
- const metricId = '8';
- const alertPath = 'alert';
- const relevantQueries = [{ metricId, alert_path: alertPath, label: 'alert-label' }];
- const dataTrackingOptions = {
- create: { action: 'click_button', label: 'create_alert' },
- delete: { action: 'click_button', label: 'delete_alert' },
- update: { action: 'click_button', label: 'update_alert' },
- };
-
- const defaultProps = {
- disabled: false,
- relevantQueries,
- modalId: 'alert-modal-1',
- };
-
- const propsWithAlertData = {
- ...defaultProps,
- alertsToManage: {
- alert: {
- alert_path: alertPath,
- operator: '<',
- threshold: 5,
- metricId,
- runbookUrl: INVALID_URL,
- },
- },
- configuredAlert: metricId,
- };
-
- function createComponent(props = {}) {
- const propsData = {
- ...defaultProps,
- ...props,
- };
-
- wrapper = shallowMount(AlertWidgetForm, {
- propsData,
- stubs: {
- GlModal: ModalStub,
- },
- });
- }
-
- const modal = () => wrapper.find(ModalStub);
- const modalTitle = () => modal().attributes('title');
- const submitButton = () => modal().find(GlLink);
- const findRunbookField = () => modal().find('[data-testid="alertRunbookField"]');
- const findThresholdField = () => modal().find('[data-qa-selector="alert_threshold_field"]');
- const submitButtonTrackingOpts = () =>
- JSON.parse(submitButton().attributes('data-tracking-options'));
- const stubEvent = { preventDefault: jest.fn() };
-
- afterEach(() => {
- if (wrapper) wrapper.destroy();
- });
-
- it('disables the form when disabled prop is set', () => {
- createComponent({ disabled: true });
-
- expect(modal().attributes('ok-disabled')).toBe('true');
- });
-
- it('disables the form if no query is selected', () => {
- createComponent();
-
- expect(modal().attributes('ok-disabled')).toBe('true');
- });
-
- it('shows correct title and button text', () => {
- createComponent();
-
- expect(modalTitle()).toBe('Add alert');
- expect(submitButton().text()).toBe('Add');
- });
-
- it('sets tracking options for create alert', () => {
- createComponent();
-
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create);
- });
-
- it('emits a "create" event when form submitted without existing alert', async () => {
- createComponent(defaultProps);
-
- modal().vm.$emit('shown');
-
- findThresholdField().vm.$emit('input', 900);
- findRunbookField().vm.$emit('input', INVALID_URL);
-
- modal().vm.$emit('ok', stubEvent);
-
- expect(wrapper.emitted().create[0]).toEqual([
- {
- alert: undefined,
- operator: '>',
- threshold: 900,
- prometheus_metric_id: '8',
- runbookUrl: INVALID_URL,
- },
- ]);
- });
-
- it('resets form when modal is dismissed (hidden)', () => {
- createComponent(defaultProps);
-
- modal().vm.$emit('shown');
-
- findThresholdField().vm.$emit('input', 800);
- findRunbookField().vm.$emit('input', INVALID_URL);
-
- modal().vm.$emit('hidden');
-
- expect(wrapper.vm.selectedAlert).toEqual({});
- expect(wrapper.vm.operator).toBe(null);
- expect(wrapper.vm.threshold).toBe(null);
- expect(wrapper.vm.prometheusMetricId).toBe(null);
- expect(wrapper.vm.runbookUrl).toBe(null);
- });
-
- it('sets selectedAlert to the provided configuredAlert on modal show', () => {
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
-
- expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
- });
-
- it('sets selectedAlert to the first relevantQueries if there is only one option on modal show', () => {
- createComponent({
- ...propsWithAlertData,
- configuredAlert: '',
- });
-
- modal().vm.$emit('shown');
-
- expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
- });
-
- it('does not set selectedAlert to the first relevantQueries if there is more than one option on modal show', () => {
- createComponent({
- relevantQueries: [
- {
- metricId: '8',
- alertPath: 'alert',
- label: 'alert-label',
- },
- {
- metricId: '9',
- alertPath: 'alert',
- label: 'alert-label',
- },
- ],
- });
-
- modal().vm.$emit('shown');
-
- expect(wrapper.vm.selectedAlert).toEqual({});
- });
-
- describe('with existing alert', () => {
- beforeEach(() => {
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
- });
-
- it('sets tracking options for delete alert', () => {
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.delete);
- });
-
- it('updates button text', () => {
- expect(modalTitle()).toBe('Edit alert');
- expect(submitButton().text()).toBe('Delete');
- });
-
- it('emits "delete" event when form values unchanged', () => {
- modal().vm.$emit('ok', stubEvent);
-
- expect(wrapper.emitted().delete[0]).toEqual([
- {
- alert: 'alert',
- operator: '<',
- threshold: 5,
- prometheus_metric_id: '8',
- runbookUrl: INVALID_URL,
- },
- ]);
- });
- });
-
- it('emits "update" event when form changed', () => {
- const updatedRunbookUrl = `${INVALID_URL}/test`;
-
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
-
- findRunbookField().vm.$emit('input', updatedRunbookUrl);
- findThresholdField().vm.$emit('input', 11);
-
- modal().vm.$emit('ok', stubEvent);
-
- expect(wrapper.emitted().update[0]).toEqual([
- {
- alert: 'alert',
- operator: '<',
- threshold: 11,
- prometheus_metric_id: '8',
- runbookUrl: updatedRunbookUrl,
- },
- ]);
- });
-
- it('sets tracking options for update alert', async () => {
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
-
- findThresholdField().vm.$emit('input', 11);
-
- await wrapper.vm.$nextTick();
-
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
- });
-
- describe('alert runbooks', () => {
- it('shows the runbook field', () => {
- createComponent();
-
- expect(findRunbookField().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index c44fd8dce33..8dc6132709e 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -159,10 +159,6 @@ describe('Anomaly chart component', () => {
const { deploymentData } = getTimeSeriesProps();
expect(deploymentData).toEqual(anomalyDeploymentData);
});
- it('"thresholds" keeps the same value', () => {
- const { thresholds } = getTimeSeriesProps();
- expect(thresholds).toEqual(inputThresholds);
- });
it('"projectPath" keeps the same value', () => {
const { projectPath } = getTimeSeriesProps();
expect(projectPath).toEqual(mockProjectPath);
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index ea6e4f4a5ed..27f7489aa49 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -643,7 +643,6 @@ describe('Time series component', () => {
expect(props.data).toBe(wrapper.vm.chartData);
expect(props.option).toBe(wrapper.vm.chartOptions);
expect(props.formatTooltipText).toBe(wrapper.vm.formatTooltipText);
- expect(props.thresholds).toBe(wrapper.vm.thresholds);
});
it('receives a tooltip title', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index 8af6075a416..400ac2e8f85 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -28,7 +28,6 @@ describe('dashboard invalid url parameters', () => {
},
},
options,
- provide: { hasManagedPrometheus: false },
});
};
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index c8951dff9ed..9a73dc820af 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -5,7 +5,6 @@ import Vuex from 'vuex';
import { setTestTimeout } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import invalidUrl from '~/lib/utils/invalid_url';
-import AlertWidget from '~/monitoring/components/alert_widget.vue';
import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
@@ -28,7 +27,6 @@ import {
barGraphData,
} from '../graph_data';
import {
- mockAlert,
mockLogsHref,
mockLogsPath,
mockNamespace,
@@ -56,7 +54,6 @@ describe('Dashboard Panel', () => {
const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
const findMenuItems = () => wrapper.findAll(GlDropdownItem);
const findMenuItemByText = (text) => findMenuItems().filter((i) => i.text() === text);
- const findAlertsWidget = () => wrapper.find(AlertWidget);
const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(DashboardPanel, {
@@ -80,9 +77,6 @@ describe('Dashboard Panel', () => {
});
};
- const setMetricsSavedToDb = (val) =>
- monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
-
beforeEach(() => {
setTestTimeout(1000);
@@ -601,42 +595,6 @@ describe('Dashboard Panel', () => {
});
});
- describe('panel alerts', () => {
- beforeEach(() => {
- mockGetterReturnValue('metricsSavedToDb', []);
-
- createWrapper();
- });
-
- describe.each`
- desc | metricsSavedToDb | props | isShown
- ${'with permission and no metrics in db'} | ${[]} | ${{}} | ${false}
- ${'with permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{}} | ${true}
- ${'without permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{ prometheusAlertsAvailable: false }} | ${false}
- ${'with permission and unrelated metrics in db'} | ${['another_metric_id']} | ${{}} | ${false}
- `('$desc', ({ metricsSavedToDb, isShown, props }) => {
- const showsDesc = isShown ? 'shows' : 'does not show';
-
- beforeEach(() => {
- setMetricsSavedToDb(metricsSavedToDb);
- createWrapper({
- alertsEndpoint: '/endpoint',
- prometheusAlertsAvailable: true,
- ...props,
- });
- return wrapper.vm.$nextTick();
- });
-
- it(`${showsDesc} alert widget`, () => {
- expect(findAlertsWidget().exists()).toBe(isShown);
- });
-
- it(`${showsDesc} alert configuration`, () => {
- expect(findMenuItemByText('Alerts').exists()).toBe(isShown);
- });
- });
- });
-
describe('When graphData contains links', () => {
const findManageLinksItem = () => wrapper.find({ ref: 'manageLinksItem' });
const mockLinks = [
@@ -730,13 +688,6 @@ describe('Dashboard Panel', () => {
describe('Runbook url', () => {
const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
- const { metricId } = graphData.metrics[0];
- const { alert_path: alertPath } = mockAlert;
-
- const mockRunbookAlert = {
- ...mockAlert,
- metricId,
- };
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
@@ -747,62 +698,5 @@ describe('Dashboard Panel', () => {
expect(findRunbookLinks().length).toBe(0);
});
-
- describe('when alerts are present', () => {
- beforeEach(() => {
- setMetricsSavedToDb([metricId]);
-
- createWrapper({
- alertsEndpoint: '/endpoint',
- prometheusAlertsAvailable: true,
- });
- });
-
- it('does not show a runbook link when a runbook is not set', async () => {
- findAlertsWidget().vm.$emit('setAlerts', alertPath, {
- ...mockRunbookAlert,
- runbookUrl: '',
- });
-
- await wrapper.vm.$nextTick();
-
- expect(findRunbookLinks().length).toBe(0);
- });
-
- it('shows a runbook link when a runbook is set', async () => {
- findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert);
-
- await wrapper.vm.$nextTick();
-
- expect(findRunbookLinks().length).toBe(1);
- expect(findRunbookLinks().at(0).attributes('href')).toBe(invalidUrl);
- });
- });
-
- describe('managed alert deprecation feature flag', () => {
- beforeEach(() => {
- setMetricsSavedToDb([metricId]);
- });
-
- it('shows alerts when alerts are not deprecated', () => {
- createWrapper(
- { alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
- { provide: { glFeatures: { managedAlertsDeprecation: false } } },
- );
-
- expect(findAlertsWidget().exists()).toBe(true);
- expect(findMenuItemByText('Alerts').exists()).toBe(true);
- });
-
- it('hides alerts when alerts are deprecated', () => {
- createWrapper(
- { alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
- { provide: { glFeatures: { managedAlertsDeprecation: true } } },
- );
-
- expect(findAlertsWidget().exists()).toBe(false);
- expect(findMenuItemByText('Alerts').exists()).toBe(false);
- });
- });
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index f899580b3df..9331048bce3 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -46,7 +46,6 @@ describe('Dashboard', () => {
stubs: {
DashboardHeader,
},
- provide: { hasManagedPrometheus: false },
...options,
});
};
@@ -60,9 +59,6 @@ describe('Dashboard', () => {
'dashboard-panel': true,
'dashboard-header': DashboardHeader,
},
- provide: {
- hasManagedPrometheus: false,
- },
...options,
});
};
@@ -412,7 +408,7 @@ describe('Dashboard', () => {
});
});
- describe('when all requests have been commited by the store', () => {
+ describe('when all requests have been committed by the store', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentEnvironmentName: 'production',
@@ -460,7 +456,7 @@ describe('Dashboard', () => {
it('shows the links section', () => {
expect(wrapper.vm.shouldShowLinksSection).toBe(true);
- expect(wrapper.find(LinksSection)).toExist();
+ expect(wrapper.findComponent(LinksSection).exists()).toBe(true);
});
});
@@ -807,29 +803,4 @@ describe('Dashboard', () => {
expect(dashboardPanel.exists()).toBe(true);
});
});
-
- describe('alerts deprecation', () => {
- beforeEach(() => {
- setupStoreWithData(store);
- });
-
- const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
-
- it.each`
- managedAlertsDeprecation | hasManagedPrometheus | isVisible
- ${false} | ${false} | ${false}
- ${false} | ${true} | ${true}
- ${true} | ${false} | ${false}
- ${true} | ${true} | ${false}
- `(
- 'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
- ({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
- createMountedWrapper(
- {},
- { provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } } },
- );
- expect(findDeprecationNotice().exists()).toBe(isVisible);
- },
- );
- });
});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index bea263f143a..e6785f34597 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -31,7 +31,6 @@ describe('dashboard invalid url parameters', () => {
store,
stubs: { 'graph-group': true, 'dashboard-panel': true, 'dashboard-header': DashboardHeader },
...options,
- provide: { hasManagedPrometheus: false },
});
};
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
index 8fc287c50e4..e37abf6722a 100644
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ b/spec/frontend/monitoring/components/links_section_spec.js
@@ -1,5 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+
import LinksSection from '~/monitoring/components/links_section.vue';
import { createStore } from '~/monitoring/stores';
@@ -26,12 +28,12 @@ describe('Links Section component', () => {
createShallowWrapper();
});
- it('does not render a section if no links are present', () => {
+ it('does not render a section if no links are present', async () => {
setState();
- return wrapper.vm.$nextTick(() => {
- expect(findLinks()).not.toExist();
- });
+ await nextTick();
+
+ expect(findLinks().length).toBe(0);
});
it('renders a link inside a section', () => {
diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index 28e02dff4bf..c879803fddd 100644
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -15,12 +15,12 @@ describe('Text variable component', () => {
});
};
- const findInput = () => wrapper.find(GlFormInput);
+ const findInput = () => wrapper.findComponent(GlFormInput);
it('renders a text input when all props are passed', () => {
createShallowWrapper();
- expect(findInput()).toExist();
+ expect(findInput().exists()).toBe(true);
});
it('always has a default value', () => {
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
index dbe9cc21ad5..c5a8b50ee60 100644
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ b/spec/frontend/monitoring/pages/dashboard_page_spec.js
@@ -29,7 +29,7 @@ describe('monitoring/pages/dashboard_page', () => {
});
};
- const findDashboardComponent = () => wrapper.find(Dashboard);
+ const findDashboardComponent = () => wrapper.findComponent(Dashboard);
beforeEach(() => {
buildRouter();
@@ -60,7 +60,7 @@ describe('monitoring/pages/dashboard_page', () => {
smallEmptyState: false,
};
- expect(findDashboardComponent()).toExist();
+ expect(findDashboardComponent().exists()).toBe(true);
expect(allProps).toMatchObject(findDashboardComponent().props());
});
});
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
index 2a712d4361f..b027d60f61e 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -20,8 +20,6 @@ const MockApp = {
template: `<router-view :dashboard-props="dashboardProps"/>`,
};
-const provide = { hasManagedPrometheus: false };
-
describe('Monitoring router', () => {
let router;
let store;
@@ -39,7 +37,6 @@ describe('Monitoring router', () => {
localVue,
store,
router,
- provide,
});
};
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index 9db0f823d84..c454d502beb 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -53,7 +53,7 @@ describe('DiscussionCounter component', () => {
describe('has no resolvable discussions', () => {
it('does not render', () => {
- store.commit(types.SET_INITIAL_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
+ store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue });
@@ -64,7 +64,7 @@ describe('DiscussionCounter component', () => {
describe('has resolvable discussions', () => {
const updateStore = (note = {}) => {
discussionMock.notes[0] = { ...discussionMock.notes[0], ...note };
- store.commit(types.SET_INITIAL_DISCUSSIONS, [discussionMock]);
+ store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussionMock]);
store.dispatch('updateResolvableDiscussionsCounts');
};
@@ -97,7 +97,7 @@ describe('DiscussionCounter component', () => {
let toggleAllButton;
const updateStoreWithExpanded = (expanded) => {
const discussion = { ...discussionMock, expanded };
- store.commit(types.SET_INITIAL_DISCUSSIONS, [discussion]);
+ store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue });
toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 59ac75f00e6..ff840a55535 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,6 +1,7 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
import { SYSTEM_NOTE } from '~/notes/constants';
@@ -26,6 +27,9 @@ describe('DiscussionNotes', () => {
const createComponent = (props, mountingMethod = shallowMount) => {
wrapper = mountingMethod(DiscussionNotes, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: {
discussion: discussionMock,
isExpanded: false,
diff --git a/spec/frontend/notes/components/multiline_comment_form_spec.js b/spec/frontend/notes/components/multiline_comment_form_spec.js
index b6d603c6358..b027a261c15 100644
--- a/spec/frontend/notes/components/multiline_comment_form_spec.js
+++ b/spec/frontend/notes/components/multiline_comment_form_spec.js
@@ -50,18 +50,6 @@ describe('MultilineCommentForm', () => {
expect(wrapper.vm.commentLineStart).toEqual(lineRange.start);
expect(setSelectedCommentPosition).toHaveBeenCalled();
});
-
- it('sets commentLineStart to selectedCommentPosition', () => {
- const notes = {
- selectedCommentPosition: {
- start: { ...testLine },
- },
- };
- const wrapper = createWrapper({}, { notes });
-
- expect(wrapper.vm.commentLineStart).toEqual(wrapper.vm.selectedCommentPosition.start);
- expect(setSelectedCommentPosition).not.toHaveBeenCalled();
- });
});
describe('destroyed', () => {
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 40251244423..4e345c9ac8d 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -58,7 +58,6 @@ describe('issue_note_body component', () => {
it('adds autosave', () => {
const autosaveKey = `autosave/Note/${note.noteable_type}/${note.id}`;
- expect(vm.autosave).toExist();
expect(vm.autosave.key).toEqual(autosaveKey);
});
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index abc888cd245..48bfd6eac5a 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -1,3 +1,4 @@
+import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
@@ -91,6 +92,7 @@ describe('issue_note_form component', () => {
expect(conflictWarning.exists()).toBe(true);
expect(conflictWarning.text().replace(/\s+/g, ' ').trim()).toBe(message);
+ expect(conflictWarning.find(GlLink).attributes('href')).toBe('#note_545');
});
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 727ef02dcbb..6aab60edc4e 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
import { trimText } from 'helpers/text_helper';
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
@@ -31,6 +32,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: { discussion: discussionMock },
});
});
@@ -167,6 +171,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: { discussion: discussionMock },
});
});
@@ -185,6 +192,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: { discussion: discussionMock },
});
});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 241a89b2218..b3dbc26878f 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -2,11 +2,14 @@ import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Vue from 'vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { setTestTimeout } from 'helpers/timeout';
+import waitForPromises from 'helpers/wait_for_promises';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
import * as constants from '~/notes/constants';
@@ -76,6 +79,9 @@ describe('note_app', () => {
</div>`,
},
{
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData,
store,
},
@@ -430,4 +436,57 @@ describe('note_app', () => {
);
});
});
+
+ describe('fetching discussions', () => {
+ describe('when note anchor is not present', () => {
+ it('does not include extra query params', async () => {
+ wrapper = shallowMount(NotesApp, { propsData, store: createStore() });
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toBeUndefined();
+ });
+ });
+
+ describe('when note anchor is present', () => {
+ const mountWithNotesFilter = (notesFilter) =>
+ shallowMount(NotesApp, {
+ propsData: {
+ ...propsData,
+ notesData: {
+ ...propsData.notesData,
+ notesFilter,
+ },
+ },
+ store: createStore(),
+ });
+
+ beforeEach(() => {
+ setWindowLocation('#note_1');
+ });
+
+ it('does not include extra query params when filter is undefined', async () => {
+ wrapper = mountWithNotesFilter(undefined);
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toBeUndefined();
+ });
+
+ it('does not include extra query params when filter is already set to default', async () => {
+ wrapper = mountWithNotesFilter(constants.DISCUSSION_FILTERS_DEFAULT_VALUE);
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toBeUndefined();
+ });
+
+ it('includes extra query params when filter is not set to default', async () => {
+ wrapper = mountWithNotesFilter(constants.COMMENTS_ONLY_FILTER_VALUE);
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toEqual({
+ notes_filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
+ persist_filter: false,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 6a6e47ffcc5..26a072b82f8 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import { setHTMLFixture } from 'helpers/fixtures';
import createEventHub from '~/helpers/event_hub_factory';
@@ -7,12 +8,15 @@ import eventHub from '~/notes/event_hub';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
import notesModule from '~/notes/stores/modules';
+let scrollToFile;
const discussion = (id, index) => ({
id,
resolvable: index % 2 === 0,
active: true,
notes: [{}],
diff_discussion: true,
+ position: { new_line: 1, old_line: 1 },
+ diff_file: { file_path: 'test.js' },
});
const createDiscussions = () => [...'abcde'].map(discussion);
const createComponent = () => ({
@@ -45,6 +49,7 @@ describe('Discussion navigation mixin', () => {
jest.spyOn(utils, 'scrollToElement');
expandDiscussion = jest.fn();
+ scrollToFile = jest.fn();
const { actions, ...notesRest } = notesModule();
store = new Vuex.Store({
modules: {
@@ -52,6 +57,10 @@ describe('Discussion navigation mixin', () => {
...notesRest,
actions: { ...actions, expandDiscussion },
},
+ diffs: {
+ namespaced: true,
+ actions: { scrollToFile },
+ },
},
});
store.state.notes.discussions = createDiscussions();
@@ -136,6 +145,7 @@ describe('Discussion navigation mixin', () => {
it('scrolls to element', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
+ { behavior: 'smooth' },
);
});
});
@@ -163,6 +173,7 @@ describe('Discussion navigation mixin', () => {
expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
findDiscussion('ul.notes', expected),
+ { behavior: 'smooth' },
);
});
});
@@ -203,10 +214,60 @@ describe('Discussion navigation mixin', () => {
it('scrolls to discussion', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
+ { behavior: 'smooth' },
);
});
});
});
});
+
+ describe.each`
+ diffsVirtualScrolling
+ ${false}
+ ${true}
+ `('virtual scrolling feature is $diffsVirtualScrolling', ({ diffsVirtualScrolling }) => {
+ beforeEach(() => {
+ window.gon = { features: { diffsVirtualScrolling } };
+
+ jest.spyOn(store, 'dispatch');
+
+ store.state.notes.currentDiscussionId = 'a';
+ window.location.hash = 'test';
+ });
+
+ afterEach(() => {
+ window.gon = {};
+ window.location.hash = '';
+ });
+
+ it('resets location hash if diffsVirtualScrolling flag is true', async () => {
+ wrapper.vm.jumpToNextDiscussion();
+
+ await nextTick();
+
+ expect(window.location.hash).toBe(diffsVirtualScrolling ? '' : '#test');
+ });
+
+ it.each`
+ tabValue | hashValue
+ ${'diffs'} | ${false}
+ ${'show'} | ${!diffsVirtualScrolling}
+ ${'other'} | ${!diffsVirtualScrolling}
+ `(
+ 'calls scrollToFile with setHash as $hashValue when the tab is $tabValue',
+ async ({ hashValue, tabValue }) => {
+ window.mrTabs.currentAction = tabValue;
+
+ wrapper.vm.jumpToNextDiscussion();
+
+ await nextTick();
+
+ expect(store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
+ path: 'test.js',
+ setHash: hashValue,
+ });
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 2ff65d3f47e..bbe074f0105 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -119,7 +119,7 @@ describe('Actions Notes Store', () => {
actions.setInitialNotes,
[individualNote],
{ notes: [] },
- [{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }],
+ [{ type: 'ADD_OR_UPDATE_DISCUSSIONS', payload: [individualNote] }],
[],
done,
);
@@ -1395,4 +1395,93 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('fetchDiscussions', () => {
+ const discussion = { notes: [] };
+
+ afterEach(() => {
+ window.gon = {};
+ });
+
+ it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', (done) => {
+ axiosMock.onAny().reply(200, { discussion });
+ testAction(
+ actions.fetchDiscussions,
+ {},
+ null,
+ [
+ { type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } },
+ { type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
+ ],
+ [{ type: 'updateResolvableDiscussionsCounts' }],
+ done,
+ );
+ });
+
+ it('dispatches `fetchDiscussionsBatch` action if `paginatedIssueDiscussions` feature flag is enabled', (done) => {
+ window.gon = { features: { paginatedIssueDiscussions: true } };
+
+ testAction(
+ actions.fetchDiscussions,
+ { path: 'test-path', filter: 'test-filter', persistFilter: 'test-persist-filter' },
+ null,
+ [],
+ [
+ {
+ type: 'fetchDiscussionsBatch',
+ payload: {
+ config: {
+ params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' },
+ },
+ path: 'test-path',
+ perPage: 20,
+ },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('fetchDiscussionsBatch', () => {
+ const discussion = { notes: [] };
+
+ const config = {
+ params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' },
+ };
+
+ const actionPayload = { config, path: 'test-path', perPage: 20 };
+
+ it('updates the discussions and dispatches `updateResolvableDiscussionsCounts if there are no headers', (done) => {
+ axiosMock.onAny().reply(200, { discussion }, {});
+ testAction(
+ actions.fetchDiscussionsBatch,
+ actionPayload,
+ null,
+ [
+ { type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } },
+ { type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
+ ],
+ [{ type: 'updateResolvableDiscussionsCounts' }],
+ done,
+ );
+ });
+
+ it('dispatches itself if there is `x-next-page-cursor` header', (done) => {
+ axiosMock.onAny().reply(200, { discussion }, { 'x-next-page-cursor': 1 });
+ testAction(
+ actions.fetchDiscussionsBatch,
+ actionPayload,
+ null,
+ [{ type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } }],
+ [
+ {
+ type: 'fetchDiscussionsBatch',
+ payload: { ...actionPayload, perPage: 30, cursor: 1 },
+ },
+ ],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 99e24f724f4..c9e24039b64 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -159,7 +159,7 @@ describe('Notes Store mutations', () => {
});
});
- describe('SET_INITIAL_DISCUSSIONS', () => {
+ describe('ADD_OR_UPDATE_DISCUSSIONS', () => {
it('should set the initial notes received', () => {
const state = {
discussions: [],
@@ -169,15 +169,17 @@ describe('Notes Store mutations', () => {
individual_note: true,
notes: [
{
+ id: 100,
note: '1',
},
{
+ id: 101,
note: '2',
},
],
};
- mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]);
+ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [note, legacyNote]);
expect(state.discussions[0].id).toEqual(note.id);
expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note);
@@ -190,7 +192,7 @@ describe('Notes Store mutations', () => {
discussions: [],
};
- mutations.SET_INITIAL_DISCUSSIONS(state, [
+ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [
{
...note,
diff_file: {
@@ -208,7 +210,7 @@ describe('Notes Store mutations', () => {
discussions: [],
};
- mutations.SET_INITIAL_DISCUSSIONS(state, [
+ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [
{
...note,
diff_file: {
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 dbebdeeb452..67e2594d29f 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
@@ -2,11 +2,11 @@
exports[`packages_list_app renders 1`] = `
<div>
- <div
- help-url="foo"
+ <infrastructure-title-stub
+ helpurl="foo"
/>
- <div />
+ <infrastructure-search-stub />
<div>
<section
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 b94192c531c..5f7555a3a2b 100644
--- a/spec/frontend/packages/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_app_spec.js
@@ -9,6 +9,7 @@ import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import * as packageUtils from '~/packages_and_registries/shared/utils';
+import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
@@ -26,18 +27,9 @@ describe('packages_list_app', () => {
};
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
- // we need to manually stub dynamic imported components because shallowMount is not able to stub them automatically. See: https://github.com/vuejs/vue-test-utils/issues/1279
- const PackageSearch = { name: 'PackageSearch', template: '<div></div>' };
- const PackageTitle = { name: 'PackageTitle', template: '<div></div>' };
- const InfrastructureTitle = { name: 'InfrastructureTitle', template: '<div></div>' };
- const InfrastructureSearch = { name: 'InfrastructureSearch', template: '<div></div>' };
-
const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.find(GlEmptyState);
const findListComponent = () => wrapper.find(PackageList);
- const findPackageSearch = () => wrapper.find(PackageSearch);
- const findPackageTitle = () => wrapper.find(PackageTitle);
- const findInfrastructureTitle = () => wrapper.find(InfrastructureTitle);
const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
const createStore = (filter = []) => {
@@ -66,10 +58,6 @@ describe('packages_list_app', () => {
PackageList,
GlSprintf,
GlLink,
- PackageSearch,
- PackageTitle,
- InfrastructureTitle,
- InfrastructureSearch,
},
provide,
});
@@ -191,48 +179,23 @@ describe('packages_list_app', () => {
});
});
- describe('Package Search', () => {
+ describe('Search', () => {
it('exists', () => {
mountComponent();
- expect(findPackageSearch().exists()).toBe(true);
+ expect(findInfrastructureSearch().exists()).toBe(true);
});
it('on update fetches data from the store', () => {
mountComponent();
store.dispatch.mockClear();
- findPackageSearch().vm.$emit('update');
+ findInfrastructureSearch().vm.$emit('update');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
});
- describe('Infrastructure config', () => {
- it('defaults to package registry components', () => {
- mountComponent();
-
- expect(findPackageSearch().exists()).toBe(true);
- expect(findPackageTitle().exists()).toBe(true);
-
- expect(findInfrastructureTitle().exists()).toBe(false);
- expect(findInfrastructureSearch().exists()).toBe(false);
- });
-
- it('mount different component based on the provided values', () => {
- mountComponent({
- titleComponent: 'InfrastructureTitle',
- searchComponent: 'InfrastructureSearch',
- });
-
- expect(findPackageSearch().exists()).toBe(false);
- expect(findPackageTitle().exists()).toBe(false);
-
- expect(findInfrastructureTitle().exists()).toBe(true);
- expect(findInfrastructureSearch().exists()).toBe(true);
- });
- });
-
describe('delete alert handling', () => {
const originalLocation = window.location.href;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
diff --git a/spec/frontend/packages/list/components/packages_search_spec.js b/spec/frontend/packages/list/components/packages_search_spec.js
deleted file mode 100644
index 30fad74b493..00000000000
--- a/spec/frontend/packages/list/components/packages_search_spec.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import component from '~/packages/list/components/package_search.vue';
-import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
-import { sortableFields } from '~/packages/list/utils';
-import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('Package Search', () => {
- let wrapper;
- let store;
-
- const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
- const findUrlSync = () => wrapper.findComponent(UrlSync);
-
- const createStore = (isGroupPage) => {
- const state = {
- config: {
- isGroupPage,
- },
- sorting: {
- orderBy: 'version',
- sort: 'desc',
- },
- filter: [],
- };
- store = new Vuex.Store({
- state,
- });
- store.dispatch = jest.fn();
- };
-
- const mountComponent = (isGroupPage = false) => {
- createStore(isGroupPage);
-
- wrapper = shallowMount(component, {
- localVue,
- store,
- stubs: {
- UrlSync,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('has a registry search component', () => {
- mountComponent();
-
- expect(findRegistrySearch().exists()).toBe(true);
- expect(findRegistrySearch().props()).toMatchObject({
- filter: store.state.filter,
- sorting: store.state.sorting,
- tokens: expect.arrayContaining([
- expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
- ]),
- sortableFields: sortableFields(),
- });
- });
-
- it.each`
- isGroupPage | page
- ${false} | ${'project'}
- ${true} | ${'group'}
- `('in a $page page binds the right props', ({ isGroupPage }) => {
- mountComponent(isGroupPage);
-
- expect(findRegistrySearch().props()).toMatchObject({
- filter: store.state.filter,
- sorting: store.state.sorting,
- tokens: expect.arrayContaining([
- expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
- ]),
- sortableFields: sortableFields(isGroupPage),
- });
- });
-
- it('on sorting:changed emits update event and calls vuex setSorting', () => {
- const payload = { sort: 'foo' };
-
- mountComponent();
-
- findRegistrySearch().vm.$emit('sorting:changed', payload);
-
- expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
- expect(wrapper.emitted('update')).toEqual([[]]);
- });
-
- it('on filter:changed calls vuex setFilter', () => {
- const payload = ['foo'];
-
- mountComponent();
-
- findRegistrySearch().vm.$emit('filter:changed', payload);
-
- expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
- });
-
- it('on filter:submit emits update event', () => {
- mountComponent();
-
- findRegistrySearch().vm.$emit('filter:submit');
-
- expect(wrapper.emitted('update')).toEqual([[]]);
- });
-
- it('has a UrlSync component', () => {
- mountComponent();
-
- expect(findUrlSync().exists()).toBe(true);
- });
-
- it('on query:changed calls updateQuery from UrlSync', () => {
- jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
-
- mountComponent();
-
- findRegistrySearch().vm.$emit('query:changed');
-
- expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/packages/list/components/packages_title_spec.js b/spec/frontend/packages/list/components/packages_title_spec.js
deleted file mode 100644
index a17f72e3133..00000000000
--- a/spec/frontend/packages/list/components/packages_title_spec.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list//constants';
-import PackageTitle from '~/packages/list/components/package_title.vue';
-import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-
-describe('PackageTitle', () => {
- let wrapper;
- let store;
-
- const findTitleArea = () => wrapper.find(TitleArea);
- const findMetadataItem = () => wrapper.find(MetadataItem);
-
- const mountComponent = (propsData = { helpUrl: 'foo' }) => {
- wrapper = shallowMount(PackageTitle, {
- store,
- propsData,
- stubs: {
- TitleArea,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('title area', () => {
- it('exists', () => {
- mountComponent();
-
- expect(findTitleArea().exists()).toBe(true);
- });
-
- it('has the correct props', () => {
- mountComponent();
-
- expect(findTitleArea().props()).toMatchObject({
- title: LIST_TITLE_TEXT,
- infoMessages: [{ text: LIST_INTRO_TEXT, link: 'foo' }],
- });
- });
- });
-
- describe.each`
- count | exist | text
- ${null} | ${false} | ${''}
- ${undefined} | ${false} | ${''}
- ${0} | ${true} | ${'0 Packages'}
- ${1} | ${true} | ${'1 Package'}
- ${2} | ${true} | ${'2 Packages'}
- `('when count is $count metadata item', ({ count, exist, text }) => {
- beforeEach(() => {
- mountComponent({ count, helpUrl: 'foo' });
- });
-
- it(`is ${exist} that it exists`, () => {
- expect(findMetadataItem().exists()).toBe(exist);
- });
-
- if (exist) {
- it('has the correct props', () => {
- expect(findMetadataItem().props()).toMatchObject({
- icon: 'package',
- text,
- });
- });
- }
- });
-});
diff --git a/spec/frontend/packages/list/components/tokens/package_type_token_spec.js b/spec/frontend/packages/list/components/tokens/package_type_token_spec.js
deleted file mode 100644
index b0cbe34f0b9..00000000000
--- a/spec/frontend/packages/list/components/tokens/package_type_token_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import component from '~/packages/list/components/tokens/package_type_token.vue';
-import { PACKAGE_TYPES } from '~/packages/list/constants';
-
-describe('packages_filter', () => {
- let wrapper;
-
- const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
- const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
-
- const mountComponent = ({ attrs, listeners } = {}) => {
- wrapper = shallowMount(component, {
- attrs,
- listeners,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('it binds all of his attrs to filtered search token', () => {
- mountComponent({ attrs: { foo: 'bar' } });
-
- expect(findFilteredSearchToken().attributes('foo')).toBe('bar');
- });
-
- it('it binds all of his events to filtered search token', () => {
- const clickListener = jest.fn();
- mountComponent({ listeners: { click: clickListener } });
-
- findFilteredSearchToken().vm.$emit('click');
-
- expect(clickListener).toHaveBeenCalled();
- });
-
- it.each(PACKAGE_TYPES.map((p, index) => [p, index]))(
- 'displays a suggestion for %p',
- (packageType, index) => {
- mountComponent();
- const item = findFilteredSearchSuggestions().at(index);
- expect(item.text()).toBe(packageType.title);
- expect(item.props('value')).toBe(packageType.type);
- },
- );
-});
diff --git a/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
index f80e2ce6ecc..7044c1285d8 100644
--- a/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -4,10 +4,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
<div
class="gl-breadcrumbs"
>
+
<ol
class="breadcrumb gl-breadcrumb-list"
>
-
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -15,24 +15,28 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class=""
href="/"
target="_self"
- />
- </li>
-
- <span
- class="gl-breadcrumb-separator"
- data-testid="separator"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="angle-right-icon"
- role="img"
>
- <use
- href="#angle-right"
- />
- </svg>
- </span>
+ <span>
+
+ </span>
+
+ <span
+ class="gl-breadcrumb-separator"
+ data-testid="separator"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s8"
+ data-testid="angle-right-icon"
+ role="img"
+ >
+ <use
+ href="#angle-right"
+ />
+ </svg>
+ </span>
+ </a>
+ </li>
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -40,10 +44,14 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class=""
href="#"
target="_self"
- />
+ >
+ <span>
+
+ </span>
+
+ <!---->
+ </a>
</li>
-
- <!---->
</ol>
</div>
`;
@@ -52,10 +60,10 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
<div
class="gl-breadcrumbs"
>
+
<ol
class="breadcrumb gl-breadcrumb-list"
>
-
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -63,10 +71,14 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
class=""
href="/"
target="_self"
- />
+ >
+ <span>
+
+ </span>
+
+ <!---->
+ </a>
</li>
-
- <!---->
</ol>
</div>
`;
diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
index 4597c42add9..6d7bf528495 100644
--- a/spec/frontend/registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
@@ -1,7 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import component from '~/registry/explorer/components/delete_button.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
describe('delete_button', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/delete_image_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
index 9a0d070e42b..620c96e8c9e 100644
--- a/spec/frontend/registry/explorer/components/delete_image_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import component from '~/registry/explorer/components/delete_image.vue';
-import { GRAPHQL_PAGE_SIZE } from '~/registry/explorer/constants/index';
-import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
-import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
+import component from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/container_registry/explorer/constants/index';
+import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
describe('Delete Image', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
index 5f191ef5561..5f191ef5561 100644
--- a/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index c2a2a4e06ea..e25162f4da5 100644
--- a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -1,13 +1,13 @@
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/details_page/delete_alert.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue';
import {
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('Delete alert', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
index d2fe5af3a94..16c9485e69e 100644
--- a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
@@ -1,13 +1,13 @@
import { GlSprintf, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import component from '~/registry/explorer/components/details_page/delete_modal.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue';
import {
REMOVE_TAG_CONFIRMATION_TEXT,
REMOVE_TAGS_CONFIRMATION_TEXT,
DELETE_IMAGE_CONFIRMATION_TITLE,
DELETE_IMAGE_CONFIRMATION_TEXT,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import { GlModal } from '../../stubs';
describe('Delete Modal', () => {
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index acff5c21940..f06300efa29 100644
--- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,12 +1,12 @@
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { GlDropdown } from 'jest/packages_and_registries/container_registry/explorer/stubs';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
-import { GlDropdown } from 'jest/registry/explorer/stubs';
-import component from '~/registry/explorer/components/details_page/details_header.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
import {
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
@@ -19,8 +19,8 @@ import {
CLEANUP_UNFINISHED_TOOLTIP,
ROOT_IMAGE_TEXT,
ROOT_IMAGE_TOOLTIP,
-} from '~/registry/explorer/constants';
-import getContainerRepositoryTagCountQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql';
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import getContainerRepositoryTagCountQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { imageTagsCountMock } from '../../mock_data';
diff --git a/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
index 14b15945631..f14284e9efe 100644
--- a/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
@@ -1,12 +1,12 @@
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/details_page/empty_state.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
import {
NO_TAGS_TITLE,
NO_TAGS_MESSAGE,
MISSING_OR_DELETED_IMAGE_TITLE,
MISSING_OR_DELETED_IMAGE_MESSAGE,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('EmptyTagsState component', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
index af8a23e412c..1a27481a828 100644
--- a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
@@ -1,7 +1,10 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
-import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '~/registry/explorer/constants';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
+import {
+ DELETE_ALERT_TITLE,
+ DELETE_ALERT_LINK_TEXT,
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('Partial Cleanup alert', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
index b079883cefd..a11b102d9a6 100644
--- a/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
@@ -1,6 +1,6 @@
import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/details_page/status_alert.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
import {
DELETE_SCHEDULED,
DELETE_FAILED,
@@ -9,7 +9,7 @@ import {
SCHEDULED_FOR_DELETION_STATUS_MESSAGE,
FAILED_DELETION_STATUS_TITLE,
FAILED_DELETION_STATUS_MESSAGE,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('Status Alert', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index a5da37a2786..00b1d03b7c2 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -4,13 +4,13 @@ import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
MISSING_MANIFEST_WARNING_TOOLTIP,
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
-} from '~/registry/explorer/constants/index';
+} from '~/packages_and_registries/container_registry/explorer/constants/index';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 51934cd074d..9a42c82d7e0 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -4,12 +4,15 @@ import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
-import component from '~/registry/explorer/components/details_page/tags_list.vue';
-import TagsListRow from '~/registry/explorer/components/details_page/tags_list_row.vue';
-import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
-import { TAGS_LIST_TITLE, REMOVE_TAGS_BUTTON_TITLE } from '~/registry/explorer/constants/index';
-import getContainerRepositoryTagsQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
+import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
+import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
+import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
+import {
+ TAGS_LIST_TITLE,
+ REMOVE_TAGS_BUTTON_TITLE,
+} from '~/packages_and_registries/container_registry/explorer/constants/index';
+import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
index 40d84d9d4a5..060dc9dc5f3 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/details_page/tags_loader.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
import { GlSkeletonLoader } from '../../stubs';
describe('TagsLoader component', () => {
diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
index 56579847468..56579847468 100644
--- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
index 46b07b4c2d6..46b07b4c2d6 100644
--- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
diff --git a/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index 8f2c049a357..e8ddad2d8ca 100644
--- a/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -1,6 +1,6 @@
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
+import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
@@ -10,7 +10,7 @@ import {
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('cleanup_status', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
index 8ca8fca65ed..4039fba869b 100644
--- a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
@@ -1,7 +1,7 @@
import { GlDropdown } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue';
+import QuickstartDropdown from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
@@ -10,7 +10,7 @@ import {
COPY_BUILD_TITLE,
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import Tracking from '~/tracking';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
diff --git a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
index 989a60625e2..027cdf732bc 100644
--- a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import groupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
+import groupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
import { GlEmptyState } from '../../stubs';
const localVue = createLocalVue();
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index db0f869ab52..411bef54e40 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -2,9 +2,9 @@ import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import DeleteButton from '~/registry/explorer/components/delete_button.vue';
-import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
-import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
+import DeleteButton from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
+import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
@@ -12,7 +12,7 @@ import {
IMAGE_DELETE_SCHEDULED_STATUS,
SCHEDULED_STATUS,
ROOT_IMAGE_TEXT,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { imagesListResponse } from '../../mock_data';
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
index d7dd825ca3e..e0119954ed4 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
@@ -1,7 +1,7 @@
import { GlKeysetPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Component from '~/registry/explorer/components/list_page/image_list.vue';
-import ImageListRow from '~/registry/explorer/components/list_page/image_list_row.vue';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue';
+import ImageListRow from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue';
import { imagesListResponse, pageInfo as defaultPageInfo } from '../../mock_data';
diff --git a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
index 111aa45f231..21748ae2813 100644
--- a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import projectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
+import projectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
import { dockerCommands } from '../../mock_data';
import { GlEmptyState } from '../../stubs';
diff --git a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index 11a3acd9eb9..92cfeb7633e 100644
--- a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -1,11 +1,11 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Component from '~/registry/explorer/components/list_page/registry_header.vue';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_TEXT,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
jest.mock('~/lib/utils/datetime_utility', () => ({
diff --git a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js
index 487f33594c1..e5a8438f23f 100644
--- a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/registry_breadcrumb.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue';
describe('Registry Breadcrumb', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index 6a835a28807..6a835a28807 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 21af9dcc60f..adc9a64e5c9 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import DeleteImage from '~/registry/explorer/components/delete_image.vue';
-import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
-import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
-import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
-import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
-import StatusAlert from '~/registry/explorer/components/details_page/status_alert.vue';
-import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
-import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
+import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import DeleteAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue';
+import DetailsHeader from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
+import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
+import PartialCleanupAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
+import StatusAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
+import TagsList from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
+import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
import {
UNFINISHED_STATUS,
@@ -19,11 +19,11 @@ import {
ALERT_DANGER_IMAGE,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
-} from '~/registry/explorer/constants';
-import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
-import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
-import component from '~/registry/explorer/pages/details.vue';
+import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue';
import Tracking from '~/tracking';
import {
diff --git a/spec/frontend/registry/explorer/pages/index_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
index b5f718b3e61..5f4cb8969bc 100644
--- a/spec/frontend/registry/explorer/pages/index_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/pages/index.vue';
+import component from '~/packages_and_registries/container_registry/explorer/pages/index.vue';
describe('List Page', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index e1f24a2b65b..051d1e2a169 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -7,25 +7,25 @@ import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
-import DeleteImage from '~/registry/explorer/components/delete_image.vue';
-import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
-import GroupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
-import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
-import ProjectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
-import RegistryHeader from '~/registry/explorer/components/list_page/registry_header.vue';
+import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import CliCommands from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
+import GroupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
+import ImageList from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue';
+import ProjectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
+import RegistryHeader from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
SORT_FIELDS,
-} from '~/registry/explorer/constants';
-import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
-import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
-import component from '~/registry/explorer/pages/list.vue';
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
+import component from '~/packages_and_registries/container_registry/explorer/pages/list.vue';
import Tracking from '~/tracking';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { $toast } from '../../shared/mocks';
+import { $toast } from 'jest/packages_and_registries/shared/mocks';
import {
graphQLImageListMock,
graphQLImageDeleteMock,
diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js
index 4f65e73d3fa..7d281a53a59 100644
--- a/spec/frontend/registry/explorer/stubs.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js
@@ -6,7 +6,7 @@ import {
} from '@gitlab/ui';
import { RouterLinkStub } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
+import RealDeleteModal from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue';
import RealListItem from '~/vue_shared/components/registry/list_item.vue';
export const GlModal = stubComponent(RealGlModal, {
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index 1f0252965b0..625f00a8666 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -1,32 +1,40 @@
-import { GlFormInputGroup, GlFormGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlFormGroup,
+ GlSkeletonLoader,
+ GlSprintf,
+ GlEmptyState,
+} from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
-import { proxyDetailsQuery, proxyData } from './mock_data';
+import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data';
const localVue = createLocalVue();
describe('DependencyProxyApp', () => {
let wrapper;
let apolloProvider;
+ let resolver;
const provideDefaults = {
groupPath: 'gitlab-org',
dependencyProxyAvailable: true,
+ noManifestsIllustration: 'noManifestsIllustration',
};
- function createComponent({
- provide = provideDefaults,
- resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()),
- } = {}) {
+ function createComponent({ provide = provideDefaults } = {}) {
localVue.use(VueApollo);
const requestHandlers = [[getDependencyProxyDetailsQuery, resolver]];
@@ -53,6 +61,12 @@ describe('DependencyProxyApp', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findMainArea = () => wrapper.findByTestId('main-area');
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
+ const findManifestList = () => wrapper.findComponent(ManifestsList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
+ });
afterEach(() => {
wrapper.destroy();
@@ -78,8 +92,8 @@ describe('DependencyProxyApp', () => {
});
it('does not call the graphql endpoint', async () => {
- const resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
- createComponent({ ...createComponentArguments, resolver });
+ resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
+ createComponent({ ...createComponentArguments });
await waitForPromises();
@@ -145,14 +159,73 @@ describe('DependencyProxyApp', () => {
it('from group has a description with proxy count', () => {
expect(findProxyCountText().text()).toBe('Contains 2 blobs of images (1024 Bytes)');
});
+
+ describe('manifest lists', () => {
+ describe('when there are no manifests', () => {
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(
+ proxyDetailsQuery({
+ extend: { dependencyProxyManifests: { nodes: [], pageInfo: pagination() } },
+ }),
+ );
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('shows the empty state message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: provideDefaults.noManifestsIllustration,
+ title: DependencyProxyApp.i18n.noManifestTitle,
+ });
+ });
+
+ it('hides the list', () => {
+ expect(findManifestList().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are manifests', () => {
+ it('hides the empty state message', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('shows list', () => {
+ expect(findManifestList().props()).toMatchObject({
+ manifests: proxyManifests(),
+ pagination: stripTypenames(pagination()),
+ });
+ });
+
+ it('prev-page event on list fetches the previous page', () => {
+ findManifestList().vm.$emit('prev-page');
+
+ expect(resolver).toHaveBeenCalledWith({
+ before: pagination().startCursor,
+ first: null,
+ fullPath: provideDefaults.groupPath,
+ last: GRAPHQL_PAGE_SIZE,
+ });
+ });
+
+ it('next-page event on list fetches the next page', () => {
+ findManifestList().vm.$emit('next-page');
+
+ expect(resolver).toHaveBeenCalledWith({
+ after: pagination().endCursor,
+ first: GRAPHQL_PAGE_SIZE,
+ fullPath: provideDefaults.groupPath,
+ });
+ });
+ });
+ });
});
+
describe('when the dependency proxy is disabled', () => {
beforeEach(() => {
- createComponent({
- resolver: jest
- .fn()
- .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } })),
- });
+ resolver = jest
+ .fn()
+ .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } }));
+ createComponent();
return waitForPromises();
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
new file mode 100644
index 00000000000..9e4c747a1bd
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -0,0 +1,84 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { stripTypenames } from 'helpers/graphql_helpers';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+
+import Component from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
+import {
+ proxyManifests,
+ pagination,
+} from 'jest/packages_and_registries/dependency_proxy/mock_data';
+
+describe('Manifests List', () => {
+ let wrapper;
+
+ const defaultProps = {
+ manifests: proxyManifests(),
+ pagination: stripTypenames(pagination()),
+ };
+
+ const createComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(Component, {
+ propsData,
+ });
+ };
+
+ const findRows = () => wrapper.findAllComponents(ManifestRow);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has the correct title', () => {
+ createComponent();
+
+ expect(wrapper.text()).toContain(Component.i18n.listTitle);
+ });
+
+ it('shows a row for every manifest', () => {
+ createComponent();
+
+ expect(findRows().length).toBe(defaultProps.manifests.length);
+ });
+
+ it('binds a manifest to each row', () => {
+ createComponent();
+
+ expect(findRows().at(0).props()).toMatchObject({
+ manifest: defaultProps.manifests[0],
+ });
+ });
+
+ describe('pagination', () => {
+ it('is hidden when there is no next or prev pages', () => {
+ createComponent({ ...defaultProps, pagination: {} });
+
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ it('has the correct props', () => {
+ createComponent();
+
+ expect(findPagination().props()).toMatchObject({
+ ...defaultProps.pagination,
+ });
+ });
+
+ it('emits the next-page event', () => {
+ createComponent();
+
+ findPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toEqual([[]]);
+ });
+
+ it('emits the prev-page event', () => {
+ createComponent();
+
+ findPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toEqual([[]]);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
new file mode 100644
index 00000000000..b7cbd875497
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
@@ -0,0 +1,59 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import Component from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+import { proxyManifests } from 'jest/packages_and_registries/dependency_proxy/mock_data';
+
+describe('Manifest Row', () => {
+ let wrapper;
+
+ const defaultProps = {
+ manifest: proxyManifests()[0],
+ };
+
+ const createComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(Component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ TimeagoTooltip,
+ ListItem,
+ },
+ });
+ };
+
+ const findListItem = () => wrapper.findComponent(ListItem);
+ const findCachedMessages = () => wrapper.findByTestId('cached-message');
+ const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has a list item', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
+
+ it('displays the name', () => {
+ expect(wrapper.text()).toContain('alpine');
+ });
+
+ it('displays the version', () => {
+ expect(wrapper.text()).toContain('latest');
+ });
+
+ it('displays the cached time', () => {
+ expect(findCachedMessages().text()).toContain('Cached');
+ });
+
+ it('has a time ago tooltip component', () => {
+ expect(findTimeAgoTooltip().props()).toMatchObject({
+ time: defaultProps.manifest.createdAt,
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
index 23d42e109f9..8bad22b5287 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
@@ -7,7 +7,21 @@ export const proxyData = () => ({
export const proxySettings = (extend = {}) => ({ enabled: true, ...extend });
-export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({
+export const proxyManifests = () => [
+ { createdAt: '2021-09-22T09:45:28Z', imageName: 'alpine:latest' },
+ { createdAt: '2021-09-21T09:45:28Z', imageName: 'alpine:stable' },
+];
+
+export const pagination = (extend) => ({
+ endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'eyJpZCI6IjI0NyIsIm5hbWUiOiJ2ZXJzaW9uX3Rlc3QxIn0',
+ __typename: 'PageInfo',
+ ...extend,
+});
+
+export const proxyDetailsQuery = ({ extendSettings = {}, extend } = {}) => ({
data: {
group: {
...proxyData(),
@@ -16,6 +30,11 @@ export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({
...proxySettings(extendSettings),
__typename: 'DependencyProxySetting',
},
+ dependencyProxyManifests: {
+ nodes: proxyManifests(),
+ pageInfo: pagination(),
+ },
+ ...extend,
},
},
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
index 451cf743e35..519014bb9cf 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
@@ -19,15 +19,15 @@ exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
>
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ <h2
+ class="gl-font-size-h1 gl-mt-3 gl-mb-0"
data-testid="title"
>
@gitlab-org/package-15
- </h1>
+ </h2>
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
<gl-icon-stub
class="gl-mr-3"
@@ -117,15 +117,15 @@ exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
>
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ <h2
+ class="gl-font-size-h1 gl-mt-3 gl-mb-0"
data-testid="title"
>
@gitlab-org/package-15
- </h1>
+ </h2>
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
<gl-icon-stub
class="gl-mr-3"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
index 8f69f943112..c95538546c1 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
@@ -27,6 +27,7 @@ exports[`VersionRow renders 1`] = `
>
<span
class="gl-truncate"
+ data-testid="truncate-end-container"
title="@gitlab-org/package-15"
>
<span
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
index 5119512564f..0bea84693f6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
@@ -16,16 +16,15 @@ import PackageFiles from '~/packages_and_registries/package_registry/components/
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
- DELETE_PACKAGE_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
PACKAGE_TYPE_NUGET,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import {
@@ -34,8 +33,6 @@ import {
packageVersions,
dependencyLinks,
emptyPackageDetailsQuery,
- packageDestroyMutation,
- packageDestroyMutationError,
packageFiles,
packageDestroyFileMutation,
packageDestroyFileMutationError,
@@ -64,14 +61,12 @@ describe('PackagesApp', () => {
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
- mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
} = {}) {
localVue.use(VueApollo);
const requestHandlers = [
[getPackageDetails, resolver],
- [destroyPackageMutation, mutationResolver],
[destroyPackageFileMutation, fileDeleteMutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
@@ -82,6 +77,7 @@ describe('PackagesApp', () => {
provide,
stubs: {
PackageTitle,
+ DeletePackage,
GlModal: {
template: '<div></div>',
methods: {
@@ -108,6 +104,7 @@ describe('PackagesApp', () => {
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
+ const findDeletePackage = () => wrapper.findComponent(DeletePackage);
afterEach(() => {
wrapper.destroy();
@@ -187,14 +184,6 @@ describe('PackagesApp', () => {
});
};
- const performDeletePackage = async () => {
- await findDeleteButton().trigger('click');
-
- findDeleteModal().vm.$emit('primary');
-
- await waitForPromises();
- };
-
afterEach(() => {
Object.defineProperty(document, 'referrer', {
value: originalReferrer,
@@ -220,7 +209,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- await performDeletePackage();
+ findDeletePackage().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'projectListUrl?showSuccessDeleteAlert=true',
@@ -234,45 +223,13 @@ describe('PackagesApp', () => {
await waitForPromises();
- await performDeletePackage();
+ findDeletePackage().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'groupListUrl?showSuccessDeleteAlert=true',
);
});
});
-
- describe('request failure', () => {
- it('on global failure it displays an alert', async () => {
- createComponent({ mutationResolver: jest.fn().mockRejectedValue() });
-
- await waitForPromises();
-
- await performDeletePackage();
-
- expect(createFlash).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_ERROR_MESSAGE,
- }),
- );
- });
-
- it('on payload with error it displays an alert', async () => {
- createComponent({
- mutationResolver: jest.fn().mockResolvedValue(packageDestroyMutationError()),
- });
-
- await waitForPromises();
-
- await performDeletePackage();
-
- expect(createFlash).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_ERROR_MESSAGE,
- }),
- );
- });
- });
});
describe('package files', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
index b24946c8638..8bb05b00e65 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
@@ -33,12 +33,12 @@ describe('InstallationCommands', () => {
});
}
- const npmInstallation = () => wrapper.find(NpmInstallation);
- const mavenInstallation = () => wrapper.find(MavenInstallation);
- const conanInstallation = () => wrapper.find(ConanInstallation);
- const nugetInstallation = () => wrapper.find(NugetInstallation);
- const pypiInstallation = () => wrapper.find(PypiInstallation);
- const composerInstallation = () => wrapper.find(ComposerInstallation);
+ const npmInstallation = () => wrapper.findComponent(NpmInstallation);
+ const mavenInstallation = () => wrapper.findComponent(MavenInstallation);
+ const conanInstallation = () => wrapper.findComponent(ConanInstallation);
+ const nugetInstallation = () => wrapper.findComponent(NugetInstallation);
+ const pypiInstallation = () => wrapper.findComponent(PypiInstallation);
+ const composerInstallation = () => wrapper.findComponent(ComposerInstallation);
afterEach(() => {
wrapper.destroy();
@@ -57,7 +57,7 @@ describe('InstallationCommands', () => {
it(`${packageEntity.packageType} instructions exist`, () => {
createComponent({ packageEntity });
- expect(selector()).toExist();
+ expect(selector().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
new file mode 100644
index 00000000000..5de30829fa5
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
@@ -0,0 +1,160 @@
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import createFlash from '~/flash';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+
+import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
+import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
+import {
+ packageDestroyMutation,
+ packageDestroyMutationError,
+ packagesListQuery,
+} from '../../mock_data';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+
+describe('DeletePackage', () => {
+ let wrapper;
+ let apolloProvider;
+ let resolver;
+ let mutationResolver;
+
+ const eventPayload = { id: '1' };
+
+ function createComponent(propsData = {}) {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getPackagesQuery, resolver],
+ [destroyPackageMutation, mutationResolver],
+ ];
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(DeletePackage, {
+ propsData,
+ localVue,
+ apolloProvider,
+ scopedSlots: {
+ default(props) {
+ return this.$createElement('button', {
+ attrs: {
+ 'data-testid': 'trigger-button',
+ },
+ on: {
+ click: props.deletePackage,
+ },
+ });
+ },
+ },
+ });
+ }
+
+ const findButton = () => wrapper.findByTestId('trigger-button');
+
+ const clickOnButtonAndWait = (payload) => {
+ findButton().trigger('click', payload);
+ return waitForPromises();
+ };
+
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(packagesListQuery());
+ mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation());
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('binds deletePackage method to the default slot', () => {
+ createComponent();
+
+ findButton().trigger('click');
+
+ expect(wrapper.emitted('start')).toEqual([[]]);
+ });
+
+ it('calls apollo mutation', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ });
+
+ it('passes refetchQueries to apollo mutate', async () => {
+ const variables = { isGroupPage: true };
+ createComponent({
+ refetchQueries: [{ query: getPackagesQuery, variables }],
+ });
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ expect(resolver).toHaveBeenCalledWith(variables);
+ });
+
+ describe('on mutation success', () => {
+ it('emits end event', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(wrapper.emitted('end')).toEqual([[]]);
+ });
+
+ it('does not call createFlash', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('calls createFlash with the success message when showSuccessAlert is true', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DeletePackage.i18n.successMessage,
+ type: 'success',
+ });
+ });
+ });
+
+ describe.each`
+ errorType | mutationResolverResponse
+ ${'connectionError'} | ${jest.fn().mockRejectedValue()}
+ ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())}
+ `('on mutation $errorType', ({ mutationResolverResponse }) => {
+ beforeEach(() => {
+ mutationResolver = mutationResolverResponse;
+ });
+
+ it('emits end event', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(wrapper.emitted('end')).toEqual([[]]);
+ });
+
+ it('calls createFlash with the error message', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DeletePackage.i18n.errorMessage,
+ type: 'warning',
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
index 1b556be5873..5af75868084 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
@@ -8,5 +8,62 @@ exports[`PackagesListApp renders 1`] = `
/>
<package-search-stub />
+
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt=""
+ class="gl-max-w-full"
+ role="img"
+ src="emptyListIllustration"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="emptyListHelpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-justify-content-center"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
index 3958cdf21bb..ad848f367e0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
@@ -2,22 +2,25 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackageListApp from '~/packages_and_registries/package_registry/components/list/app.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
+import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
- LIST_QUERY_DEBOUNCE_TIME,
+ GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
-import { packagesListQuery } from '../../mock_data';
+import { packagesListQuery, packageData, pagination } from '../../mock_data';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
@@ -39,11 +42,20 @@ describe('PackagesListApp', () => {
const PackageList = {
name: 'package-list',
template: '<div><slot name="empty-state"></slot></div>',
+ props: OriginalPackageList.props,
};
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
+ const searchPayload = {
+ sort: 'VERSION_DESC',
+ filters: { packageName: 'foo', packageType: 'CONAN' },
+ };
+
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
+ const findListComponent = () => wrapper.findComponent(PackageList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findDeletePackage = () => wrapper.findComponent(DeletePackage);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -61,9 +73,10 @@ describe('PackagesListApp', () => {
stubs: {
GlEmptyState,
GlLoadingIcon,
- PackageList,
GlSprintf,
GlLink,
+ PackageList,
+ DeletePackage,
},
});
};
@@ -72,15 +85,24 @@ describe('PackagesListApp', () => {
wrapper.destroy();
});
- const waitForDebouncedApollo = () => {
- jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+ const waitForFirstRequest = () => {
+ // emit a search update so the query is executed
+ findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] });
return waitForPromises();
};
+ it('does not execute the query without sort being set', () => {
+ const resolver = jest.fn().mockResolvedValue(packagesListQuery());
+
+ mountComponent({ resolver });
+
+ expect(resolver).not.toHaveBeenCalled();
+ });
+
it('renders', async () => {
mountComponent();
- await waitForDebouncedApollo();
+ await waitForFirstRequest();
expect(wrapper.element).toMatchSnapshot();
});
@@ -88,7 +110,7 @@ describe('PackagesListApp', () => {
it('has a package title', async () => {
mountComponent();
- await waitForDebouncedApollo();
+ await waitForFirstRequest();
expect(findPackageTitle().exists()).toBe(true);
expect(findPackageTitle().props('count')).toBe(2);
@@ -105,25 +127,54 @@ describe('PackagesListApp', () => {
const resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
- const payload = {
- sort: 'VERSION_DESC',
- filters: { packageName: 'foo', packageType: 'CONAN' },
- };
-
- findSearch().vm.$emit('update', payload);
+ findSearch().vm.$emit('update', searchPayload);
- await waitForDebouncedApollo();
- jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({
- groupSort: payload.sort,
- ...payload.filters,
+ groupSort: searchPayload.sort,
+ ...searchPayload.filters,
}),
);
});
});
+ describe('list component', () => {
+ let resolver;
+
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(packagesListQuery());
+ mountComponent({ resolver });
+
+ return waitForFirstRequest();
+ });
+
+ it('exists and has the right props', () => {
+ expect(findListComponent().props()).toMatchObject({
+ list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
+ isLoading: false,
+ pageInfo: expect.objectContaining({ endCursor: pagination().endCursor }),
+ });
+ });
+
+ it('when list emits next-page fetches the next set of records', () => {
+ findListComponent().vm.$emit('next-page');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }),
+ );
+ });
+
+ it('when list emits prev-page fetches the prev set of records', () => {
+ findListComponent().vm.$emit('prev-page');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
+ );
+ });
+ });
+
describe.each`
type | sortType
${PROJECT_RESOURCE_TYPE} | ${'sort'}
@@ -136,9 +187,9 @@ describe('PackagesListApp', () => {
beforeEach(() => {
provide = { ...defaultProvide, isGroupPage };
- resolver = jest.fn().mockResolvedValue(packagesListQuery(type));
+ resolver = jest.fn().mockResolvedValue(packagesListQuery({ type }));
mountComponent({ provide, resolver });
- return waitForDebouncedApollo();
+ return waitForFirstRequest();
});
it('succeeds', () => {
@@ -147,8 +198,85 @@ describe('PackagesListApp', () => {
it('calls the resolver with the right parameters', () => {
expect(resolver).toHaveBeenCalledWith(
- expect.objectContaining({ isGroupPage, [sortType]: '' }),
+ expect.objectContaining({ isGroupPage, [sortType]: 'NAME_DESC' }),
);
});
});
+
+ describe('empty state', () => {
+ beforeEach(() => {
+ const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } }));
+ mountComponent({ resolver });
+
+ return waitForFirstRequest();
+ });
+ it('generate the correct empty list link', () => {
+ const link = findListComponent().findComponent(GlLink);
+
+ expect(link.attributes('href')).toBe(defaultProvide.emptyListHelpUrl);
+ expect(link.text()).toBe('publish and share your packages');
+ });
+
+ it('includes the right content on the default tab', () => {
+ expect(findEmptyState().text()).toContain(PackageListApp.i18n.emptyPageTitle);
+ });
+ });
+
+ describe('filter without results', () => {
+ beforeEach(async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findSearch().vm.$emit('update', searchPayload);
+
+ return nextTick();
+ });
+
+ it('should show specific empty message', () => {
+ expect(findEmptyState().text()).toContain(PackageListApp.i18n.noResultsTitle);
+ expect(findEmptyState().text()).toContain(PackageListApp.i18n.widenFilters);
+ });
+ });
+
+ describe('delete package', () => {
+ it('exists and has the correct props', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ expect(findDeletePackage().props()).toMatchObject({
+ refetchQueries: [{ query: getPackagesQuery, variables: {} }],
+ showSuccessAlert: true,
+ });
+ });
+
+ it('deletePackage is bound to package-list package:delete event', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('package:delete', { id: 1 });
+
+ expect(findDeletePackage().emitted('start')).toEqual([[]]);
+ });
+
+ it('start and end event set loading correctly', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findDeletePackage().vm.$emit('start');
+
+ await nextTick();
+
+ expect(findListComponent().props('isLoading')).toBe(true);
+
+ findDeletePackage().vm.$emit('end');
+
+ await nextTick();
+
+ expect(findListComponent().props('isLoading')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index b624e66482d..de4e9c8ae5b 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -1,93 +1,86 @@
-import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import { last } from 'lodash';
-import Vuex from 'vuex';
-import stubChildren from 'helpers/stub_children';
-import { packageList } from 'jest/packages/mock_data';
+import { GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
-import { TrackingActions } from '~/packages/shared/constants';
-import * as SharedUtils from '~/packages/shared/utils';
+import {
+ DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import { packageData } from '../../mock_data';
describe('packages_list', () => {
let wrapper;
- let store;
+
+ const firstPackage = packageData();
+ const secondPackage = {
+ ...packageData(),
+ id: 'gid://gitlab/Packages::Package/112',
+ name: 'second-package',
+ };
+
+ const defaultProps = {
+ list: [firstPackage, secondPackage],
+ isLoading: false,
+ pageInfo: {},
+ };
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
+ const GlModalStub = {
+ name: GlModal.name,
+ template: '<div><slot></slot></div>',
+ methods: { show: jest.fn() },
+ };
- const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
- const findPackageListPagination = () => wrapper.find(GlPagination);
- const findPackageListDeleteModal = () => wrapper.find(GlModal);
- const findEmptySlot = () => wrapper.find(EmptySlotStub);
- const findPackagesListRow = () => wrapper.find(PackagesListRow);
-
- const createStore = (isGroupPage, packages, isLoading) => {
- const state = {
- isLoading,
- packages,
- pagination: {
- perPage: 1,
- total: 1,
- page: 1,
- },
- config: {
- isGroupPage,
+ const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
+ const findPackageListPagination = () => wrapper.findComponent(GlKeysetPagination);
+ const findPackageListDeleteModal = () => wrapper.findComponent(GlModalStub);
+ const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
+ const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMountExtended(PackagesList, {
+ propsData: {
+ ...defaultProps,
+ ...props,
},
- sorting: {
- orderBy: 'version',
- sort: 'desc',
+ stubs: {
+ GlModal: GlModalStub,
+ GlSprintf,
},
- };
- store = new Vuex.Store({
- state,
- getters: {
- getList: () => packages,
+ slots: {
+ 'empty-state': EmptySlotStub,
},
});
- store.dispatch = jest.fn();
};
- const mountComponent = ({
- isGroupPage = false,
- packages = packageList,
- isLoading = false,
- ...options
- } = {}) => {
- createStore(isGroupPage, packages, isLoading);
-
- wrapper = mount(PackagesList, {
- localVue,
- store,
- stubs: {
- ...stubChildren(PackagesList),
- GlTable,
- GlModal,
- },
- ...options,
- });
- };
+ beforeEach(() => {
+ GlModalStub.methods.show.mockReset();
+ });
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('when is loading', () => {
beforeEach(() => {
- mountComponent({
- packages: [],
- isLoading: true,
- });
+ mountComponent({ isLoading: true });
});
- it('shows skeleton loader when loading', () => {
+ it('shows skeleton loader', () => {
expect(findPackagesListLoader().exists()).toBe(true);
});
+
+ it('does not show the rows', () => {
+ expect(findPackagesListRow().exists()).toBe(false);
+ });
+
+ it('does not show the pagination', () => {
+ expect(findPackageListPagination().exists()).toBe(false);
+ });
});
describe('when is not loading', () => {
@@ -95,74 +88,61 @@ describe('packages_list', () => {
mountComponent();
});
- it('does not show skeleton loader when not loading', () => {
+ it('does not show skeleton loader', () => {
expect(findPackagesListLoader().exists()).toBe(false);
});
- });
- describe('layout', () => {
- beforeEach(() => {
- mountComponent();
+ it('shows the rows', () => {
+ expect(findPackagesListRow().exists()).toBe(true);
});
+ });
+ describe('layout', () => {
it('contains a pagination component', () => {
- const sorting = findPackageListPagination();
- expect(sorting.exists()).toBe(true);
+ mountComponent({ pageInfo: { hasPreviousPage: true } });
+
+ expect(findPackageListPagination().exists()).toBe(true);
});
it('contains a modal component', () => {
- const sorting = findPackageListDeleteModal();
- expect(sorting.exists()).toBe(true);
+ mountComponent();
+
+ expect(findPackageListDeleteModal().exists()).toBe(true);
});
});
describe('when the user can destroy the package', () => {
beforeEach(() => {
mountComponent();
+ findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
+ return nextTick();
});
- it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => {
- const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
- const item = last(wrapper.vm.list);
+ it('deleting a package opens the modal', () => {
+ expect(findPackageListDeleteModal().text()).toContain(firstPackage.name);
+ });
- findPackagesListRow().vm.$emit('packageToDelete', item);
+ it('confirming on the modal emits package:delete', async () => {
+ findPackageListDeleteModal().vm.$emit('ok');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.itemToBeDeleted).toEqual(item);
- expect(mockModalShow).toHaveBeenCalled();
- });
- });
+ await nextTick();
- it('deleteItemConfirmation resets itemToBeDeleted', () => {
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemConfirmation();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
});
- it('deleteItemConfirmation emit package:delete', () => {
- const itemToBeDeleted = { id: 2 };
- wrapper.setData({ itemToBeDeleted });
- wrapper.vm.deleteItemConfirmation();
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
- });
- });
+ it('closing the modal resets itemToBeDeleted', async () => {
+ // triggering the v-model
+ findPackageListDeleteModal().vm.$emit('input', false);
- it('deleteItemCanceled resets itemToBeDeleted', () => {
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemCanceled();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ await nextTick();
+
+ expect(findPackageListDeleteModal().text()).not.toContain(firstPackage.name);
});
});
describe('when the list is empty', () => {
beforeEach(() => {
- mountComponent({
- packages: [],
- slots: {
- 'empty-state': EmptySlotStub,
- },
- });
+ mountComponent({ list: [] });
});
it('show the empty slot', () => {
@@ -171,45 +151,59 @@ describe('packages_list', () => {
});
});
- describe('pagination component', () => {
- let pagination;
- let modelEvent;
-
+ describe('pagination ', () => {
beforeEach(() => {
- mountComponent();
- pagination = findPackageListPagination();
- // retrieve the event used by v-model, a more sturdy approach than hardcoding it
- modelEvent = pagination.vm.$options.model.event;
+ mountComponent({ pageInfo: { hasPreviousPage: true } });
});
- it('emits page:changed events when the page changes', () => {
- pagination.vm.$emit(modelEvent, 2);
- expect(wrapper.emitted('page:changed')).toEqual([[2]]);
+ it('emits prev-page events when the prev event is fired', () => {
+ findPackageListPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toEqual([[]]);
+ });
+
+ it('emits next-page events when the next event is fired', () => {
+ findPackageListPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toEqual([[]]);
});
});
describe('tracking', () => {
let eventSpy;
- let utilSpy;
- const category = 'foo';
+ const category = 'UI::NpmPackages';
beforeEach(() => {
- mountComponent();
eventSpy = jest.spyOn(Tracking, 'event');
- utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
- wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
+ mountComponent();
+ findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
+ return nextTick();
});
- it('tracking category calls packageTypeToTrackCategory', () => {
- expect(wrapper.vm.tracking.category).toBe(category);
- expect(utilSpy).toHaveBeenCalledWith('conan');
+ it('requesting the delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ it('confirming delete tracks the right action', () => {
+ findPackageListDeleteModal().vm.$emit('ok');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ DELETE_PACKAGE_TRACKING_ACTION,
+ expect.any(Object),
+ );
});
- it('deleteItemConfirmation calls event', () => {
- wrapper.vm.deleteItemConfirmation();
+ it('canceling delete tracks the right action', () => {
+ findPackageListDeleteModal().vm.$emit('cancel');
+
expect(eventSpy).toHaveBeenCalledWith(
category,
- TrackingActions.DELETE_PACKAGE,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index e65b2a6f320..bed7a07ff36 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { sortableFields } from '~/packages/list/utils';
+import { sortableFields } from '~/packages_and_registries/package_registry/utils';
import component from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
index 3fa96ce1d29..e992ba12faa 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -37,8 +36,8 @@ describe('PackageTitle', () => {
mountComponent();
expect(findTitleArea().props()).toMatchObject({
- title: LIST_TITLE_TEXT,
- infoMessages: [{ text: LIST_INTRO_TEXT, link: 'foo' }],
+ title: PackageTitle.i18n.LIST_TITLE_TEXT,
+ infoMessages: [{ text: PackageTitle.i18n.LIST_INTRO_TEXT, link: 'foo' }],
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
index b0cbe34f0b9..26b2f3b359f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
@@ -1,7 +1,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/packages/list/components/tokens/package_type_token.vue';
-import { PACKAGE_TYPES } from '~/packages/list/constants';
+import component from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
+import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/constants';
describe('packages_filter', () => {
let wrapper;
@@ -41,8 +41,8 @@ describe('packages_filter', () => {
(packageType, index) => {
mountComponent();
const item = findFilteredSearchSuggestions().at(index);
- expect(item.text()).toBe(packageType.title);
- expect(item.props('value')).toBe(packageType.type);
+ expect(item.text()).toBe(packageType);
+ expect(item.props('value')).toBe(packageType);
},
);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 70fc096fa44..bacc748db81 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -1,3 +1,5 @@
+import capitalize from 'lodash/capitalize';
+
export const packageTags = () => [
{ id: 'gid://gitlab/Packages::Tag/87', name: 'bananas_9', __typename: 'PackageTag' },
{ id: 'gid://gitlab/Packages::Tag/86', name: 'bananas_8', __typename: 'PackageTag' },
@@ -156,6 +158,15 @@ export const nugetMetadata = () => ({
projectUrl: 'projectUrl',
});
+export const pagination = (extend) => ({
+ endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'eyJpZCI6IjI0NyIsIm5hbWUiOiJ2ZXJzaW9uX3Rlc3QxIn0',
+ __typename: 'PageInfo',
+ ...extend,
+});
+
export const packageDetailsQuery = (extendPackage) => ({
data: {
package: {
@@ -256,7 +267,7 @@ export const packageDestroyFileMutationError = () => ({
],
});
-export const packagesListQuery = (type = 'group') => ({
+export const packagesListQuery = ({ type = 'group', extend = {}, extendPagination = {} } = {}) => ({
data: {
[type]: {
packages: {
@@ -277,9 +288,11 @@ export const packagesListQuery = (type = 'group') => ({
pipelines: { nodes: [] },
},
],
+ pageInfo: pagination(extendPagination),
__typename: 'PackageConnection',
},
- __typename: 'Group',
+ ...extend,
+ __typename: capitalize(type),
},
},
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
index c56244a9138..5c9ade7f785 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup, GlFormSelect } from 'jest/registry/shared/stubs';
+import { GlFormGroup, GlFormSelect } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_dropdown.vue';
describe('ExpirationDropdown', () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
index dd876d1d295..6b681924fcf 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
@@ -1,6 +1,6 @@
import { GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup } from 'jest/registry/shared/stubs';
+import { GlFormGroup } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_input.vue';
import { NAME_REGEX_LENGTH } from '~/packages_and_registries/settings/project/constants';
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
index 854830391c5..94f7783afe7 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
@@ -1,6 +1,6 @@
import { GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup } from 'jest/registry/shared/stubs';
+import { GlFormGroup } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_run_text.vue';
import {
NEXT_CLEANUP_LABEL,
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
index 3a3eb089b43..45039614e49 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
@@ -1,6 +1,6 @@
import { GlToggle, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup } from 'jest/registry/shared/stubs';
+import { GlFormGroup } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_toggle.vue';
import {
ENABLED_TOGGLE_DESCRIPTION,
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
index 3a71af94d5a..bc104a25ef9 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
@@ -2,7 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { GlCard, GlLoadingIcon } from 'jest/registry/shared/stubs';
+import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/settings_form.vue';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
diff --git a/spec/frontend/registry/shared/mocks.js b/spec/frontend/packages_and_registries/shared/mocks.js
index fdef38b6f10..fdef38b6f10 100644
--- a/spec/frontend/registry/shared/mocks.js
+++ b/spec/frontend/packages_and_registries/shared/mocks.js
diff --git a/spec/frontend/registry/shared/stubs.js b/spec/frontend/packages_and_registries/shared/stubs.js
index ad41eb42df4..ad41eb42df4 100644
--- a/spec/frontend/registry/shared/stubs.js
+++ b/spec/frontend/packages_and_registries/shared/stubs.js
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
index c579aa2f2da..1fcc00489e3 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -38,7 +38,7 @@ describe('Dropdown select component', () => {
it('creates a hidden input if fieldName is provided', () => {
mountDropdown({ fieldName: 'namespace-input' });
- expect(findNamespaceInput()).toExist();
+ expect(findNamespaceInput().exists()).toBe(true);
expect(findNamespaceInput().attributes('name')).toBe('namespace-input');
});
@@ -57,9 +57,9 @@ describe('Dropdown select component', () => {
// wait for dropdown options to populate
await wrapper.vm.$nextTick();
- expect(findDropdownOption('user: Administrator')).toExist();
- expect(findDropdownOption('group: GitLab Org')).toExist();
- expect(findDropdownOption('group: Foobar')).not.toExist();
+ expect(findDropdownOption('user: Administrator').exists()).toBe(true);
+ expect(findDropdownOption('group: GitLab Org').exists()).toBe(true);
+ expect(findDropdownOption('group: Foobar').exists()).toBe(false);
findDropdownOption('user: Administrator').trigger('click');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index de8b29d54fc..5bba98bdf96 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -94,13 +94,13 @@ describe('Todos', () => {
});
it('updates pending text', () => {
- expect(document.querySelector('.todos-pending .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-pending .badge').innerHTML).toEqual(
addDelimiter(TEST_COUNT_BIG),
);
});
it('updates done text', () => {
- expect(document.querySelector('.todos-done .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-done .badge').innerHTML).toEqual(
addDelimiter(TEST_DONE_COUNT_BIG),
);
});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
index 3aa0e99a858..3e371a8765f 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
@@ -135,6 +135,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Set up CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -156,6 +157,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Start a free Ultimate trial"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -177,6 +179,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add code owners"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -205,6 +208,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add merge request approval"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -269,6 +273,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Create an issue"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -290,6 +295,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Submit a merge request"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -347,6 +353,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Run a Security scan using CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
index f8099d7e95a..7e97a539a99 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
@@ -1,13 +1,17 @@
import { GlProgressBar } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
+import eventHub from '~/invite_members/event_hub';
import { testActions, testSections } from './mock_data';
describe('Learn GitLab', () => {
let wrapper;
+ let inviteMembersOpen = false;
const createWrapper = () => {
- wrapper = mount(LearnGitlab, { propsData: { actions: testActions, sections: testSections } });
+ wrapper = mount(LearnGitlab, {
+ propsData: { actions: testActions, sections: testSections, inviteMembersOpen },
+ });
};
beforeEach(() => {
@@ -17,6 +21,7 @@ describe('Learn GitLab', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ inviteMembersOpen = false;
});
it('renders correctly', () => {
@@ -35,4 +40,30 @@ describe('Learn GitLab', () => {
expect(progressBar.attributes('value')).toBe('2');
expect(progressBar.attributes('max')).toBe('9');
});
+
+ describe('Invite Members Modal', () => {
+ let spy;
+
+ beforeEach(() => {
+ spy = jest.spyOn(eventHub, '$emit');
+ });
+
+ it('emits openModal', () => {
+ inviteMembersOpen = true;
+
+ createWrapper();
+
+ expect(spy).toHaveBeenCalledWith('openModal', {
+ mode: 'celebrate',
+ inviteeType: 'members',
+ source: 'learn-gitlab',
+ });
+ });
+
+ it('does not emit openModal', () => {
+ createWrapper();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 082a8977710..9d510b3d231 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -8,9 +8,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
import {
- WIKI_CONTENT_EDITOR_TRACKING_LABEL,
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
+ WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ WIKI_FORMAT_LABEL,
+ WIKI_FORMAT_UPDATED_ACTION,
} from '~/pages/shared/wikis/constants';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
@@ -65,7 +67,6 @@ describe('WikiForm', () => {
const pageInfoPersisted = {
...pageInfoNew,
persisted: true,
-
title: 'My page',
content: ' My page content ',
format: 'markdown',
@@ -177,7 +178,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain(titleHelpText);
- expect(findTitleHelpLink().attributes().href).toEqual(titleHelpLink);
+ expect(findTitleHelpLink().attributes().href).toBe(titleHelpLink);
},
);
@@ -186,7 +187,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
- expect(findMarkdownHelpLink().attributes().href).toEqual(
+ expect(findMarkdownHelpLink().attributes().href).toBe(
'/help/user/markdown#wiki-specific-markdown',
);
});
@@ -220,8 +221,8 @@ describe('WikiForm', () => {
expect(e.preventDefault).not.toHaveBeenCalled();
});
- it('does not trigger tracking event', async () => {
- expect(trackingSpy).not.toHaveBeenCalled();
+ it('triggers wiki format tracking event', async () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
});
it('does not trim page content', () => {
@@ -273,7 +274,7 @@ describe('WikiForm', () => {
({ persisted, redirectLink }) => {
createWrapper(persisted);
- expect(findCancelButton().attributes().href).toEqual(redirectLink);
+ expect(findCancelButton().attributes().href).toBe(redirectLink);
},
);
});
@@ -438,7 +439,7 @@ describe('WikiForm', () => {
});
});
- it('triggers tracking event on form submit', async () => {
+ it('triggers tracking events on form submit', async () => {
triggerFormSubmit();
await wrapper.vm.$nextTick();
@@ -446,6 +447,15 @@ describe('WikiForm', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
+ label: WIKI_FORMAT_LABEL,
+ value: findFormat().element.value,
+ extra: {
+ old_format: pageInfoPersisted.format,
+ project_path: pageInfoPersisted.path,
+ },
+ });
});
it('updates content from content editor on form submit', async () => {
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
index 8040c9d701c..23219042008 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
@@ -5,6 +5,9 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
+const scrollIntoViewMock = jest.fn();
+HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+
describe('Pipeline Editor | Commit Form', () => {
let wrapper;
@@ -113,4 +116,20 @@ describe('Pipeline Editor | Commit Form', () => {
expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
});
});
+
+ describe('when scrollToCommitForm becomes true', () => {
+ beforeEach(async () => {
+ createComponent();
+ wrapper.setProps({ scrollToCommitForm: true });
+ await wrapper.vm.$nextTick();
+ });
+
+ it('scrolls into view', () => {
+ expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth' });
+ });
+
+ it('emits "scrolled-to-commit-form"', () => {
+ expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy();
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
index 2f934898ef1..efc345d8877 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -52,6 +52,7 @@ describe('Pipeline Editor | Commit section', () => {
const defaultProps = {
ciFileContent: mockCiYml,
commitSha: mockCommitSha,
+ isNewCiConfigFile: false,
};
const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
@@ -72,7 +73,6 @@ describe('Pipeline Editor | Commit section', () => {
data() {
return {
currentBranch: mockDefaultBranch,
- isNewCiConfigFile: Boolean(options?.isNewCiConfigfile),
};
},
mocks: {
@@ -115,7 +115,7 @@ describe('Pipeline Editor | Commit section', () => {
describe('when the user commits a new file', () => {
beforeEach(async () => {
- createComponent({ options: { isNewCiConfigfile: true } });
+ createComponent({ props: { isNewCiConfigFile: true } });
await submitCommit();
});
@@ -277,4 +277,16 @@ describe('Pipeline Editor | Commit section', () => {
expect(wrapper.emitted('resetContent')).toHaveLength(1);
});
});
+
+ it('sets listeners on commit form', () => {
+ const handler = jest.fn();
+ createComponent({ options: { listeners: { event: handler } } });
+ findCommitForm().vm.$emit('event');
+ expect(handler).toHaveBeenCalled();
+ });
+
+ it('passes down scroll-to-commit-form prop to commit form', () => {
+ createComponent({ props: { 'scroll-to-commit-form': true } });
+ expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 1b68cd3dc43..4df7768b035 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { stubExperiments } from 'helpers/experimentation_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
@@ -33,19 +34,41 @@ describe('Pipeline editor drawer', () => {
const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
+ const originalObjects = [];
+
+ beforeEach(() => {
+ originalObjects.push(window.gon, window.gl);
+ stubExperiments({ pipeline_editor_walkthrough: 'control' });
+ });
+
afterEach(() => {
wrapper.destroy();
localStorage.clear();
+ [window.gon, window.gl] = originalObjects;
});
- it('it sets the drawer to be opened by default', async () => {
- createComponent();
-
- expect(findDrawerContent().exists()).toBe(false);
-
- await nextTick();
+ describe('default expanded state', () => {
+ describe('when experiment control', () => {
+ it('sets the drawer to be opened by default', async () => {
+ createComponent();
+ expect(findDrawerContent().exists()).toBe(false);
+ await nextTick();
+ expect(findDrawerContent().exists()).toBe(true);
+ });
+ });
- expect(findDrawerContent().exists()).toBe(true);
+ describe('when experiment candidate', () => {
+ beforeEach(() => {
+ stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
+ });
+
+ it('sets the drawer to be closed by default', async () => {
+ createComponent();
+ expect(findDrawerContent().exists()).toBe(false);
+ await nextTick();
+ expect(findDrawerContent().exists()).toBe(false);
+ });
+ });
});
describe('when the drawer is collapsed', () => {
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index b5881790b0b..6532c4e289d 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -36,8 +36,9 @@ describe('Pipeline editor branch switcher', () => {
let mockLastCommitBranchQuery;
const createComponent = (
- { currentBranch, isQueryLoading, mountFn, options } = {
+ { currentBranch, isQueryLoading, mountFn, options, props } = {
currentBranch: mockDefaultBranch,
+ hasUnsavedChanges: false,
isQueryLoading: false,
mountFn: shallowMount,
options: {},
@@ -45,6 +46,7 @@ describe('Pipeline editor branch switcher', () => {
) => {
wrapper = mountFn(BranchSwitcher, {
propsData: {
+ ...props,
paginationLimit: mockBranchPaginationLimit,
},
provide: {
@@ -70,7 +72,7 @@ describe('Pipeline editor branch switcher', () => {
});
};
- const createComponentWithApollo = (mountFn = shallowMount) => {
+ const createComponentWithApollo = ({ mountFn = shallowMount, props = {} } = {}) => {
const handlers = [[getAvailableBranchesQuery, mockAvailableBranchQuery]];
const resolvers = {
Query: {
@@ -86,6 +88,7 @@ describe('Pipeline editor branch switcher', () => {
createComponent({
mountFn,
+ props,
options: {
localVue,
apolloProvider: mockApollo,
@@ -138,8 +141,8 @@ describe('Pipeline editor branch switcher', () => {
createComponentWithApollo();
});
- it('does not render dropdown', () => {
- expect(findDropdown().exists()).toBe(false);
+ it('disables the dropdown', () => {
+ expect(findDropdown().props('disabled')).toBe(true);
});
});
@@ -149,7 +152,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
@@ -186,7 +189,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('does not render dropdown', () => {
- expect(findDropdown().exists()).toBe(false);
+ expect(findDropdown().props('disabled')).toBe(true);
});
it('shows an error message', () => {
@@ -201,7 +204,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
@@ -247,6 +250,23 @@ describe('Pipeline editor branch switcher', () => {
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
+
+ describe('with unsaved changes', () => {
+ beforeEach(async () => {
+ createComponentWithApollo({ mountFn: mount, props: { hasUnsavedChanges: true } });
+ await waitForPromises();
+ });
+
+ it('emits `select-branch` event and does not switch branch', async () => {
+ expect(wrapper.emitted('select-branch')).toBeUndefined();
+
+ const branch = findDropdownItems().at(1);
+ await branch.vm.$emit('click');
+
+ expect(wrapper.emitted('select-branch')).toEqual([[branch.text()]]);
+ expect(wrapper.emitted('refetchContent')).toBeUndefined();
+ });
+ });
});
describe('when searching', () => {
@@ -255,7 +275,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
@@ -429,7 +449,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
await createNewBranch();
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
index 44656b2b67d..29ab52bde8f 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
@@ -16,7 +16,7 @@ describe('Pipeline Status', () => {
let mockApollo;
let mockPipelineQuery;
- const createComponentWithApollo = (glFeatures = {}) => {
+ const createComponentWithApollo = () => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
@@ -27,7 +27,6 @@ describe('Pipeline Status', () => {
commitSha: mockCommitSha,
},
provide: {
- glFeatures,
projectFullPath: mockProjectFullPath,
},
stubs: { GlLink, GlSprintf },
@@ -40,6 +39,8 @@ describe('Pipeline Status', () => {
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
+ const findPipelineNotTriggeredErrorMsg = () =>
+ wrapper.find('[data-testid="pipeline-not-triggered-error-msg"]');
const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]');
const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]');
const findStatusIcon = () => wrapper.find('[data-testid="pipeline-status-icon"]');
@@ -95,17 +96,18 @@ describe('Pipeline Status', () => {
it('renders pipeline data', () => {
const {
id,
+ commit: { title },
detailedStatus: { detailsPath },
} = mockProjectPipeline().pipeline;
expect(findStatusIcon().exists()).toBe(true);
expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`);
- expect(findPipelineCommit().text()).toBe(mockCommitSha);
+ expect(findPipelineCommit().text()).toBe(`${mockCommitSha}: ${title}`);
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
});
- it('does not render the pipeline mini graph', () => {
- expect(findPipelineEditorMiniGraph().exists()).toBe(false);
+ it('renders the pipeline mini graph', () => {
+ expect(findPipelineEditorMiniGraph().exists()).toBe(true);
});
});
@@ -117,7 +119,8 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('renders error', () => {
+ it('renders api error', () => {
+ expect(findPipelineNotTriggeredErrorMsg().exists()).toBe(false);
expect(findIcon().attributes('name')).toBe('warning-solid');
expect(findPipelineErrorMsg().text()).toBe(i18n.fetchError);
});
@@ -129,20 +132,22 @@ describe('Pipeline Status', () => {
expect(findPipelineViewBtn().exists()).toBe(false);
});
});
- });
- describe('when feature flag for pipeline mini graph is enabled', () => {
- beforeEach(() => {
- mockPipelineQuery.mockResolvedValue({
- data: { project: mockProjectPipeline() },
- });
+ describe('when pipeline is null', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue({
+ data: { project: { pipeline: null } },
+ });
- createComponentWithApollo({ pipelineEditorMiniGraph: true });
- waitForPromises();
- });
+ createComponentWithApollo();
+ waitForPromises();
+ });
- it('renders the pipeline mini graph', () => {
- expect(findPipelineEditorMiniGraph().exists()).toBe(true);
+ it('renders pipeline not triggered error', () => {
+ expect(findPipelineErrorMsg().exists()).toBe(false);
+ expect(findIcon().attributes('name')).toBe('information-o');
+ expect(findPipelineNotTriggeredErrorMsg().text()).toBe(i18n.pipelineNotTriggeredMsg);
+ });
});
});
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
index 3d7c3c839da..6b9f576917f 100644
--- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -1,22 +1,54 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
-import { mockProjectPipeline } from '../../mock_data';
+import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
+import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Pipeline Status', () => {
let wrapper;
+ let mockApollo;
+ let mockLinkedPipelinesQuery;
- const createComponent = ({ hasStages = true } = {}) => {
+ const createComponent = ({ hasStages = true, options } = {}) => {
wrapper = shallowMount(PipelineEditorMiniGraph, {
+ provide: {
+ dataMethod: 'graphql',
+ projectFullPath: mockProjectFullPath,
+ },
propsData: {
pipeline: mockProjectPipeline({ hasStages }).pipeline,
},
+ ...options,
+ });
+ };
+
+ const createComponentWithApollo = (hasStages = true) => {
+ const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ hasStages,
+ options: {
+ localVue,
+ apolloProvider: mockApollo,
+ },
});
};
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+ beforeEach(() => {
+ mockLinkedPipelinesQuery = jest.fn();
+ });
+
afterEach(() => {
+ mockLinkedPipelinesQuery.mockReset();
wrapper.destroy();
});
@@ -39,4 +71,38 @@ describe('Pipeline Status', () => {
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
+
+ describe('when querying upstream and downstream pipelines', () => {
+ describe('when query succeeds', () => {
+ beforeEach(() => {
+ mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
+ createComponentWithApollo();
+ });
+
+ it('should call the query with the correct variables', () => {
+ expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
+ expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
+ fullPath: mockProjectFullPath,
+ iid: mockProjectPipeline().pipeline.iid,
+ });
+ });
+ });
+
+ describe('when query fails', () => {
+ beforeEach(() => {
+ mockLinkedPipelinesQuery.mockRejectedValue(new Error());
+ createComponentWithApollo();
+ });
+
+ it('should emit an error event when query fails', async () => {
+ expect(wrapper.emitted('showError')).toHaveLength(1);
+ expect(wrapper.emitted('showError')[0]).toEqual([
+ {
+ type: PIPELINE_FAILURE,
+ reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
+ },
+ ]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 5cf8d47bc23..f6154f50bc0 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -1,19 +1,27 @@
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
+import { stubExperiments } from 'helpers/experimentation_helper';
import {
+ CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
- EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_VALID,
+ MERGED_TAB,
+ TAB_QUERY_PARAM,
+ TABS_INDEX,
} from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import { mockLintResponse, mockCiYml } from '../mock_data';
+import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
+
+Vue.config.ignoredElements = ['gl-emoji'];
describe('Pipeline editor tabs component', () => {
let wrapper;
@@ -22,6 +30,7 @@ describe('Pipeline editor tabs component', () => {
};
const createComponent = ({
+ listeners = {},
props = {},
provide = {},
appStatus = EDITOR_APP_STATUS_VALID,
@@ -31,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
propsData: {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
+ isNewCiConfigFile: true,
...props,
},
data() {
@@ -43,6 +53,7 @@ describe('Pipeline editor tabs component', () => {
TextEditor: MockTextEditor,
EditorTab,
},
+ listeners,
});
};
@@ -53,10 +64,12 @@ describe('Pipeline editor tabs component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findCiLint = () => wrapper.findComponent(CiLint);
+ const findGlTabs = () => wrapper.findComponent(GlTabs);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
const findTextEditor = () => wrapper.findComponent(MockTextEditor);
const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
+ const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover);
afterEach(() => {
wrapper.destroy();
@@ -137,7 +150,7 @@ describe('Pipeline editor tabs component', () => {
describe('when there is a fetch error', () => {
beforeEach(() => {
- createComponent({ appStatus: EDITOR_APP_STATUS_ERROR });
+ createComponent({ props: { ciConfigData: mockLintResponseWithoutMerged } });
});
it('show an error message', () => {
@@ -181,4 +194,113 @@ describe('Pipeline editor tabs component', () => {
},
);
});
+
+ describe('default tab based on url query param', () => {
+ const gitlabUrl = 'https://gitlab.test/ci/editor/';
+ const matchObject = {
+ hostname: 'gitlab.test',
+ pathname: '/ci/editor/',
+ search: '',
+ };
+
+ it(`is ${CREATE_TAB} if the query param ${TAB_QUERY_PARAM} is not present`, () => {
+ setWindowLocation(gitlabUrl);
+ createComponent();
+
+ expect(window.location).toMatchObject(matchObject);
+ });
+
+ it(`is ${CREATE_TAB} tab if the query param ${TAB_QUERY_PARAM} is invalid`, () => {
+ const queryValue = 'FOO';
+ setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${queryValue}`);
+ createComponent();
+
+ // If the query param remains unchanged, then we have ignored it.
+ expect(window.location).toMatchObject({
+ ...matchObject,
+ search: `?${TAB_QUERY_PARAM}=${queryValue}`,
+ });
+ });
+
+ it('is the tab specified in query param and transform it into an index value', async () => {
+ setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${MERGED_TAB}`);
+ createComponent();
+
+ // If the query param has changed to an index, it means we have synced the
+ // query with.
+ expect(window.location).toMatchObject({
+ ...matchObject,
+ search: `?${TAB_QUERY_PARAM}=${TABS_INDEX[MERGED_TAB]}`,
+ });
+ });
+ });
+
+ describe('glTabs', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes the `sync-active-tab-with-query-params` prop', () => {
+ expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
+ });
+ });
+
+ describe('pipeline_editor_walkthrough experiment', () => {
+ describe('when in control path', () => {
+ beforeEach(() => {
+ stubExperiments({ pipeline_editor_walkthrough: 'control' });
+ });
+
+ it('does not show walkthrough popover', async () => {
+ createComponent({ mountFn: mount });
+ await nextTick();
+ expect(findWalkthroughPopover().exists()).toBe(false);
+ });
+ });
+
+ describe('when in candidate path', () => {
+ beforeEach(() => {
+ stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
+ });
+
+ describe('when isNewCiConfigFile prop is true (default)', () => {
+ beforeEach(async () => {
+ createComponent({
+ mountFn: mount,
+ });
+ await nextTick();
+ });
+
+ it('shows walkthrough popover', async () => {
+ expect(findWalkthroughPopover().exists()).toBe(true);
+ });
+ });
+
+ describe('when isNewCiConfigFile prop is false', () => {
+ it('does not show walkthrough popover', async () => {
+ createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount });
+ await nextTick();
+ expect(findWalkthroughPopover().exists()).toBe(false);
+ });
+ });
+ });
+ });
+
+ it('sets listeners on walkthrough popover', async () => {
+ stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
+
+ const handler = jest.fn();
+
+ createComponent({
+ mountFn: mount,
+ listeners: {
+ event: handler,
+ },
+ });
+ await nextTick();
+
+ findWalkthroughPopover().vm.$emit('event');
+
+ expect(handler).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
index 9f910ed4f9c..a55176ccd79 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
@@ -11,6 +11,7 @@ import {
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
+ PIPELINE_FAILURE,
} from '~/pipeline_editor/constants';
beforeEach(() => {
@@ -65,6 +66,7 @@ describe('Pipeline Editor messages', () => {
failureType | message | expectedFailureType
${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE}
${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN}
+ ${PIPELINE_FAILURE} | ${'pipeline failure'} | ${PIPELINE_FAILURE}
${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE}
`('shows a message for $message', ({ failureType, expectedFailureType }) => {
createComponent({ failureType, showFailure: true });
diff --git a/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js b/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js
new file mode 100644
index 00000000000..a9ce89ff521
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js
@@ -0,0 +1,29 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+Vue.config.ignoredElements = ['gl-emoji'];
+
+describe('WalkthroughPopover component', () => {
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ return extendedWrapper(mountFn(WalkthroughPopover));
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('CTA button clicked', () => {
+ beforeEach(async () => {
+ wrapper = createComponent(mount);
+ await wrapper.findByTestId('ctaBtn').trigger('click');
+ });
+
+ it('emits "walkthrough-popover-cta-clicked" event', async () => {
+ expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index 0b0ff14486e..1bfc5c3b93d 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -1,4 +1,4 @@
-import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
+import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
export const mockProjectNamespace = 'user1';
@@ -35,6 +35,17 @@ job_build:
- echo "build"
needs: ["job_test_2"]
`;
+
+export const mockCiTemplateQueryResponse = {
+ data: {
+ project: {
+ ciTemplate: {
+ content: mockCiYml,
+ },
+ },
+ },
+};
+
export const mockBlobContentQueryResponse = {
data: {
project: { repository: { blobs: { nodes: [{ rawBlob: mockCiYml }] } } },
@@ -274,11 +285,14 @@ export const mockProjectPipeline = ({ hasStages = true } = {}) => {
return {
pipeline: {
- commitPath: '/-/commit/aabbccdd',
id: 'gid://gitlab/Ci::Pipeline/118',
iid: '28',
shortSha: mockCommitSha,
status: 'SUCCESS',
+ commit: {
+ title: 'Update .gitlabe-ci.yml',
+ webPath: '/-/commit/aabbccdd',
+ },
detailedStatus: {
detailsPath: '/root/sample-ci-project/-/pipelines/118',
group: 'success',
@@ -290,6 +304,62 @@ export const mockProjectPipeline = ({ hasStages = true } = {}) => {
};
};
+export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true } = {}) => {
+ let upstream = null;
+ let downstream = {
+ nodes: [],
+ __typename: 'PipelineConnection',
+ };
+
+ if (hasDownstream) {
+ downstream = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: { name: 'job-log-sections', __typename: 'Project' },
+ detailedStatus: {
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+ };
+ }
+
+ if (hasUpstream) {
+ upstream = {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: { name: 'trigger-downstream', __typename: 'Project' },
+ detailedStatus: {
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ };
+ }
+
+ return {
+ data: {
+ project: {
+ pipeline: {
+ path: '/root/ci-project/-/pipelines/790',
+ downstream,
+ upstream,
+ },
+ __typename: 'Project',
+ },
+ },
+ };
+};
+
export const mockLintResponse = {
valid: true,
mergedYaml: mockCiYml,
@@ -326,6 +396,14 @@ export const mockLintResponse = {
],
};
+export const mockLintResponseWithoutMerged = {
+ valid: false,
+ status: CI_CONFIG_STATUS_INVALID,
+ errors: ['error'],
+ warnings: [],
+ jobs: [],
+};
+
export const mockJobs = [
{
name: 'job_1',
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index b6713319e69..f6afef595c6 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,11 +1,9 @@
-import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
-import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
@@ -13,17 +11,21 @@ import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_edi
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
+
+import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
+
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+
import {
mockCiConfigPath,
mockCiConfigQueryResponse,
mockBlobContentQueryResponse,
mockBlobContentQueryResponseNoCiFile,
mockCiYml,
+ mockCiTemplateQueryResponse,
mockCommitSha,
mockCommitShaResults,
mockDefaultBranch,
@@ -35,10 +37,6 @@ import {
const localVue = createLocalVue();
localVue.use(VueApollo);
-const MockSourceEditor = {
- template: '<div/>',
-};
-
const mockProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
@@ -55,19 +53,15 @@ describe('Pipeline editor app component', () => {
let mockLatestCommitShaQuery;
let mockPipelineQuery;
- const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
+ const createComponent = ({
+ blobLoading = false,
+ options = {},
+ provide = {},
+ stubs = {},
+ } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...mockProvide, ...provide },
- stubs: {
- GlTabs,
- GlButton,
- CommitForm,
- PipelineEditorHome,
- PipelineEditorTabs,
- PipelineEditorMessages,
- SourceEditor: MockSourceEditor,
- PipelineEditorEmptyState,
- },
+ stubs,
data() {
return {
commitSha: '',
@@ -89,7 +83,7 @@ describe('Pipeline editor app component', () => {
});
};
- const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => {
+ const createComponentWithApollo = async ({ provide = {}, stubs = {} } = {}) => {
const handlers = [
[getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData],
@@ -97,7 +91,6 @@ describe('Pipeline editor app component', () => {
[getLatestCommitShaQuery, mockLatestCommitShaQuery],
[getPipelineQuery, mockPipelineQuery],
];
-
mockApollo = createMockApollo(handlers);
const options = {
@@ -105,13 +98,15 @@ describe('Pipeline editor app component', () => {
data() {
return {
currentBranch: mockDefaultBranch,
+ lastCommitBranch: '',
+ appStatus: '',
};
},
mocks: {},
apolloProvider: mockApollo,
};
- createComponent({ props, provide, options });
+ createComponent({ provide, stubs, options });
return waitForPromises();
};
@@ -119,7 +114,6 @@ describe('Pipeline editor app component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
- const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
@@ -141,7 +135,7 @@ describe('Pipeline editor app component', () => {
createComponent({ blobLoading: true });
expect(findLoadingIcon().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
});
});
@@ -185,7 +179,11 @@ describe('Pipeline editor app component', () => {
describe('when no CI config file exists', () => {
beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
- await createComponentWithApollo();
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorEmptyState,
+ },
+ });
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
@@ -206,8 +204,12 @@ describe('Pipeline editor app component', () => {
it('shows a unkown error message', async () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
- mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
- await createComponentWithApollo();
+ mockBlobContentData.mockRejectedValueOnce();
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorMessages,
+ },
+ });
expect(findEmptyState().exists()).toBe(false);
@@ -222,15 +224,20 @@ describe('Pipeline editor app component', () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
- await createComponentWithApollo();
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorHome,
+ PipelineEditorEmptyState,
+ },
+ });
expect(findEmptyState().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
await findEmptyStateButton().vm.$emit('click');
expect(findEmptyState().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(true);
+ expect(findEditorHome().exists()).toBe(true);
});
});
@@ -241,7 +248,7 @@ describe('Pipeline editor app component', () => {
describe('and the commit mutation succeeds', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
- await createComponentWithApollo();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
});
@@ -295,7 +302,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
- await createComponentWithApollo();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
@@ -319,7 +326,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
- await createComponentWithApollo();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
@@ -342,6 +349,8 @@ describe('Pipeline editor app component', () => {
describe('when refetching content', () => {
beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
@@ -377,7 +386,10 @@ describe('Pipeline editor app component', () => {
const originalLocation = window.location.href;
beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
setWindowLocation('?template=Android');
});
@@ -386,7 +398,9 @@ describe('Pipeline editor app component', () => {
});
it('renders the given template', async () => {
- await createComponentWithApollo();
+ await createComponentWithApollo({
+ stubs: { PipelineEditorHome, PipelineEditorTabs },
+ });
expect(mockGetTemplate).toHaveBeenCalledWith({
projectPath: mockProjectFullPath,
@@ -394,7 +408,40 @@ describe('Pipeline editor app component', () => {
});
expect(findEmptyState().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(true);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+
+ describe('when add_new_config_file query param is present', () => {
+ const originalLocation = window.location.href;
+
+ beforeEach(() => {
+ setWindowLocation('?add_new_config_file=true');
+
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
+ });
+
+ afterEach(() => {
+ setWindowLocation(originalLocation);
+ });
+
+ describe('when CI config file does not exist', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
+ mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
+
+ await createComponentWithApollo();
+
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+ });
+
+ it('skips empty state and shows editor home component', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 335049892ec..6f969546171 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -1,21 +1,25 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-
+import { GlModal } from '@gitlab/ui';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
-import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants';
+import { MERGED_TAB, VISUALIZE_TAB, CREATE_TAB, LINT_TAB } from '~/pipeline_editor/constants';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import { mockLintResponse, mockCiYml } from './mock_data';
+jest.mock('~/lib/utils/common_utils');
+
describe('Pipeline editor home wrapper', () => {
let wrapper;
- const createComponent = ({ props = {}, glFeatures = {} } = {}) => {
+ const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => {
wrapper = shallowMount(PipelineEditorHome, {
+ data: () => data,
propsData: {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
@@ -24,22 +28,26 @@ describe('Pipeline editor home wrapper', () => {
...props,
},
provide: {
+ projectFullPath: '',
+ totalBranches: 19,
glFeatures: {
...glFeatures,
},
},
+ stubs,
});
};
+ const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
const findCommitSection = () => wrapper.findComponent(CommitSection);
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
+ const findModal = () => wrapper.findComponent(GlModal);
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('renders', () => {
@@ -68,29 +76,103 @@ describe('Pipeline editor home wrapper', () => {
});
});
+ describe('modal when switching branch', () => {
+ describe('when `showSwitchBranchModal` value is false', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is not visible', () => {
+ expect(findModal().exists()).toBe(false);
+ });
+ });
+ describe('when `showSwitchBranchModal` value is true', () => {
+ beforeEach(() => {
+ createComponent({
+ data: { showSwitchBranchModal: true },
+ stubs: { PipelineEditorFileNav },
+ });
+ });
+
+ it('is visible', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('pass down `shouldLoadNewBranch` to the branch switcher when primary is selected', async () => {
+ expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(false);
+
+ await findModal().vm.$emit('primary');
+
+ expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(true);
+ });
+
+ it('closes the modal when secondary action is selected', async () => {
+ expect(findModal().exists()).toBe(true);
+
+ await findModal().vm.$emit('secondary');
+
+ expect(findModal().exists()).toBe(false);
+ });
+ });
+ });
+
describe('commit form toggle', () => {
beforeEach(() => {
createComponent();
});
- it('hides the commit form when in the merged tab', async () => {
- expect(findCommitSection().exists()).toBe(true);
+ it.each`
+ tab | shouldShow
+ ${MERGED_TAB} | ${false}
+ ${VISUALIZE_TAB} | ${false}
+ ${LINT_TAB} | ${false}
+ ${CREATE_TAB} | ${true}
+ `(
+ 'when the active tab is $tab the commit form is shown: $shouldShow',
+ async ({ tab, shouldShow }) => {
+ expect(findCommitSection().exists()).toBe(true);
- findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB);
- await nextTick();
- expect(findCommitSection().exists()).toBe(false);
- });
+ findPipelineEditorTabs().vm.$emit('set-current-tab', tab);
+
+ await nextTick();
- it('shows the form again when leaving the merged tab', async () => {
+ expect(findCommitSection().exists()).toBe(shouldShow);
+ },
+ );
+
+ it('shows the commit form again when coming back to the create tab', async () => {
expect(findCommitSection().exists()).toBe(true);
findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB);
await nextTick();
expect(findCommitSection().exists()).toBe(false);
- findPipelineEditorTabs().vm.$emit('set-current-tab', VISUALIZE_TAB);
+ findPipelineEditorTabs().vm.$emit('set-current-tab', CREATE_TAB);
await nextTick();
expect(findCommitSection().exists()).toBe(true);
});
});
+
+ describe('WalkthroughPopover events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when "walkthrough-popover-cta-clicked" is emitted from pipeline editor tabs', () => {
+ it('passes down `scrollToCommitForm=true` to commit section', async () => {
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
+ await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
+ });
+ });
+
+ describe('when "scrolled-to-commit-form" is emitted from commit section', () => {
+ it('passes down `scrollToCommitForm=false` to commit section', async () => {
+ await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
+ await findCommitSection().vm.$emit('scrolled-to-commit-form');
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 1af3065477d..31b74a06efd 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -35,7 +35,7 @@ describe('Pipelines Empty State', () => {
});
it('should render the CI/CD templates', () => {
- expect(pipelinesCiTemplates()).toExist();
+ expect(pipelinesCiTemplates().exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 2e8979f2b9d..db4de6deeb7 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -327,7 +327,7 @@ describe('Pipeline graph wrapper', () => {
expect(getLinksLayer().exists()).toBe(true);
expect(getLinksLayer().props('showLinks')).toBe(false);
expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
- await getDependenciesToggle().trigger('click');
+ await getDependenciesToggle().vm.$emit('change', true);
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true);
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
index 5b2a29de443..f4faa25545b 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -111,7 +111,7 @@ describe('the graph view selector component', () => {
expect(wrapper.emitted().updateShowLinksState).toBeUndefined();
expect(findToggleLoader().exists()).toBe(false);
- await findDependenciesToggle().trigger('click');
+ await findDependenciesToggle().vm.$emit('change', true);
/*
Loading happens before the event is emitted or timers are run.
Then we run the timer because the event is emitted in setInterval
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index f33c66dedf3..2d876841e06 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,15 +1,9 @@
-import { GlAlert, GlDropdown, GlDropdownItem, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
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 PipelineArtifacts, {
- i18n,
-} from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
+import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
let wrapper;
- let mockAxios;
const artifacts = [
{
@@ -21,23 +15,13 @@ describe('Pipelines Artifacts dropdown', () => {
path: '/download/path-two',
},
];
- const artifactsEndpointPlaceholder = ':pipeline_artifacts_id';
- const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
const pipelineId = 108;
- const createComponent = ({ mockData = {} } = {}) => {
+ const createComponent = ({ mockArtifacts = artifacts } = {}) => {
wrapper = shallowMount(PipelineArtifacts, {
- provide: {
- artifactsEndpoint,
- artifactsEndpointPlaceholder,
- },
propsData: {
pipelineId,
- },
- data() {
- return {
- ...mockData,
- };
+ artifacts: mockArtifacts,
},
stubs: {
GlSprintf,
@@ -45,80 +29,33 @@ describe('Pipelines Artifacts dropdown', () => {
});
};
- const findAlert = () => wrapper.findComponent(GlAlert);
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findFirstGlDropdownItem = () => wrapper.find(GlDropdownItem);
const findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem);
- beforeEach(() => {
- mockAxios = new MockAdapter(axios);
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- it('should render the dropdown', () => {
- createComponent();
-
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('should fetch artifacts on dropdown click', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
- createComponent();
- findDropdown().vm.$emit('show');
- await waitForPromises();
-
- expect(mockAxios.history.get).toHaveLength(1);
- expect(wrapper.vm.artifacts).toEqual(artifacts);
- });
-
it('should render a dropdown with all the provided artifacts', () => {
- createComponent({ mockData: { artifacts } });
+ createComponent();
expect(findAllGlDropdownItems()).toHaveLength(artifacts.length);
});
it('should render a link with the provided path', () => {
- createComponent({ mockData: { artifacts } });
+ createComponent();
expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path);
expect(findFirstGlDropdownItem().text()).toBe(artifacts[0].name);
});
- describe('with a failing request', () => {
- it('should render an error message', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(500);
- createComponent();
- findDropdown().vm.$emit('show');
- await waitForPromises();
-
- const error = findAlert();
- expect(error.exists()).toBe(true);
- expect(error.text()).toBe(i18n.artifactsFetchErrorMessage);
- });
- });
-
- describe('with no artifacts received', () => {
- it('should render empty alert message', () => {
- createComponent({ mockData: { artifacts: [] } });
-
- const emptyAlert = findAlert();
- expect(emptyAlert.exists()).toBe(true);
- expect(emptyAlert.text()).toBe(i18n.noArtifacts);
- });
- });
-
- describe('when artifacts are loading', () => {
- it('should show loading icon', () => {
- createComponent({ mockData: { isLoading: true } });
+ describe('with no artifacts', () => {
+ it('should not render the dropdown', () => {
+ createComponent({ mockArtifacts: [] });
- expect(findLoadingIcon().exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 2875498bb52..c024730570c 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -554,7 +554,7 @@ describe('Pipelines', () => {
});
it('renders the CI/CD templates', () => {
- expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true);
});
describe('when the code_quality_walkthrough experiment is active', () => {
@@ -568,7 +568,7 @@ describe('Pipelines', () => {
});
it('renders the CI/CD templates', () => {
- expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true);
});
});
@@ -597,7 +597,7 @@ describe('Pipelines', () => {
});
it('renders the CI/CD templates', () => {
- expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index fb019b463b1..6fdbe907aed 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -1,5 +1,5 @@
import '~/commons';
-import { GlTable } from '@gitlab/ui';
+import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -44,7 +44,7 @@ describe('Pipelines Table', () => {
);
};
- const findGlTable = () => wrapper.findComponent(GlTable);
+ const findGlTableLite = () => wrapper.findComponent(GlTableLite);
const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
@@ -77,7 +77,7 @@ describe('Pipelines Table', () => {
});
it('displays table', () => {
- expect(findGlTable().exists()).toBe(true);
+ expect(findGlTableLite().exists()).toBe(true);
});
it('should render table head with correct columns', () => {
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 0c8089430d0..93e2ae13628 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -3,6 +3,7 @@ import { within } from '@testing-library/dom';
import { shallowMount, mount, createWrapper } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
@@ -12,6 +13,8 @@ import eventHub from '~/projects/commit/event_hub';
import createStore from '~/projects/commit/store';
import mockData from '../mock_data';
+jest.mock('~/api');
+
describe('CommitFormModal', () => {
let wrapper;
let store;
@@ -167,4 +170,16 @@ describe('CommitFormModal', () => {
expect(findTargetProject().attributes('value')).toBe('_changed_project_value_');
});
});
+
+ it('action primary button triggers Redis HLL tracking api call', async () => {
+ createComponent(mount, {}, {}, { primaryActionEventName: 'test_event' });
+
+ await wrapper.vm.$nextTick();
+
+ jest.spyOn(findForm().element, 'submit');
+
+ getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_event');
+ });
});
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 9a8f7ff7582..60d36597fda 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -115,7 +115,7 @@ describe('Author Select', () => {
});
it('does not have popover text by default', () => {
- expect(wrapper.attributes('title')).not.toExist();
+ expect(wrapper.attributes('title')).toBeUndefined();
});
});
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 c255fcce321..e1e1aac09aa 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
@@ -52,9 +52,44 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
title="You are about to permanently delete this project"
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 repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc."
- />
+ <p>
+ This project is
+ <strong>
+ NOT
+ </strong>
+ a fork, and has the following:
+ </p>
+
+ <ul>
+ <li>
+ 1 issue
+ </li>
+
+ <li>
+ 2 merge requests
+ </li>
+
+ <li>
+ 3 forks
+ </li>
+
+ <li>
+ 4 stars
+ </li>
+ </ul>
+ After a project is permanently deleted, it
+ <strong>
+ cannot be recovered
+ </strong>
+ . Permanently deleting this project will
+ <strong>
+ immediately delete
+ </strong>
+ its repositories and
+ <strong>
+ all related resources
+ </strong>
+ , including issues, merge requests etc.
</gl-alert-stub>
<p
diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js
index 444e465ebaa..bb6021fadda 100644
--- a/spec/frontend/projects/components/project_delete_button_spec.js
+++ b/spec/frontend/projects/components/project_delete_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
import ProjectDeleteButton from '~/projects/components/project_delete_button.vue';
import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
@@ -12,6 +13,11 @@ describe('Project remove modal', () => {
const defaultProps = {
confirmPhrase: 'foo',
formPath: 'some/path',
+ isFork: false,
+ issuesCount: 1,
+ mergeRequestsCount: 2,
+ forksCount: 3,
+ starsCount: 4,
};
const createComponent = (props = {}) => {
@@ -21,6 +27,7 @@ describe('Project remove modal', () => {
...props,
},
stubs: {
+ GlSprintf,
SharedDeleteButton,
},
});
@@ -41,7 +48,10 @@ describe('Project remove modal', () => {
});
it('passes confirmPhrase and formPath props to the shared delete button', () => {
- expect(findSharedDeleteButton().props()).toEqual(defaultProps);
+ expect(findSharedDeleteButton().props()).toEqual({
+ confirmPhrase: defaultProps.confirmPhrase,
+ formPath: defaultProps.formPath,
+ });
});
});
});
diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js
index ebb2b499ead..d7308963088 100644
--- a/spec/frontend/projects/details/upload_button_spec.js
+++ b/spec/frontend/projects/details/upload_button_spec.js
@@ -1,11 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UploadButton from '~/projects/details/upload_button.vue';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
-jest.mock('~/projects/upload_file_experiment_tracking');
-
const MODAL_ID = 'details-modal-upload-blob';
describe('UploadButton', () => {
@@ -50,10 +47,6 @@ describe('UploadButton', () => {
wrapper.find(GlButton).vm.$emit('click');
});
- it('tracks the click_upload_modal_trigger event', () => {
- expect(trackFileUploadEvent).toHaveBeenCalledWith('click_upload_modal_trigger');
- });
-
it('opens the modal', () => {
expect(glModalDirective).toHaveBeenCalledWith(MODAL_ID);
});
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index aa16b71172b..b3f177a1f12 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -24,14 +24,23 @@ describe('NewProjectUrlSelect component', () => {
{
id: 'gid://gitlab/Group/26',
fullPath: 'flightjs',
+ name: 'Flight JS',
+ visibility: 'public',
+ webUrl: 'http://127.0.0.1:3000/flightjs',
},
{
id: 'gid://gitlab/Group/28',
fullPath: 'h5bp',
+ name: 'H5BP',
+ visibility: 'public',
+ webUrl: 'http://127.0.0.1:3000/h5bp',
},
{
id: 'gid://gitlab/Group/30',
fullPath: 'h5bp/subgroup',
+ name: 'H5BP Subgroup',
+ visibility: 'private',
+ webUrl: 'http://127.0.0.1:3000/h5bp/subgroup',
},
],
},
@@ -79,6 +88,10 @@ describe('NewProjectUrlSelect component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenInput = () => wrapper.find('input');
+ const clickDropdownItem = async () => {
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ };
afterEach(() => {
wrapper.destroy();
@@ -127,7 +140,6 @@ describe('NewProjectUrlSelect component', () => {
it('focuses on the input when the dropdown is opened', async () => {
wrapper = mountComponent({ mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@@ -140,7 +152,6 @@ describe('NewProjectUrlSelect component', () => {
it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@@ -160,7 +171,6 @@ describe('NewProjectUrlSelect component', () => {
beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@@ -195,23 +205,38 @@ describe('NewProjectUrlSelect component', () => {
};
wrapper = mountComponent({ search: 'no matches', queryResponse, mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.find('li').text()).toBe('No matches found');
});
- it('updates hidden input with selected namespace', async () => {
+ it('emits `update-visibility` event to update the visibility radio options', async () => {
wrapper = mountComponent();
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ const spy = jest.spyOn(eventHub, '$emit');
+ await clickDropdownItem();
+
+ const namespace = data.currentUser.groups.nodes[0];
+
+ expect(spy).toHaveBeenCalledWith('update-visibility', {
+ name: namespace.name,
+ visibility: namespace.visibility,
+ showPath: namespace.webUrl,
+ editPath: `${namespace.webUrl}/-/edit`,
+ });
+ });
+
+ it('updates hidden input with selected namespace', async () => {
+ wrapper = mountComponent();
+ jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
+ await clickDropdownItem();
+
expect(findHiddenInput().attributes()).toMatchObject({
name: 'project[namespace_id]',
value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 987a215eb4c..b4067f6a72b 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -11,6 +11,7 @@ jest.mock('~/lib/utils/url_utility');
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} };
+const ProjectQualitySummaryStub = { name: 'ProjectQualitySummary', render: () => {} };
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
@@ -23,10 +24,12 @@ describe('ProjectsPipelinesChartsApp', () => {
{
provide: {
shouldRenderDoraCharts: true,
+ shouldRenderQualitySummary: true,
},
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
LeadTimeCharts: LeadTimeChartsStub,
+ ProjectQualitySummary: ProjectQualitySummaryStub,
},
},
mountOptions,
@@ -44,6 +47,7 @@ describe('ProjectsPipelinesChartsApp', () => {
const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub);
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
const findPipelineCharts = () => wrapper.find(PipelineCharts);
+ const findProjectQualitySummary = () => wrapper.find(ProjectQualitySummaryStub);
describe('when all charts are available', () => {
beforeEach(() => {
@@ -70,6 +74,10 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findLeadTimeCharts().exists()).toBe(true);
});
+ it('renders the project quality summary', () => {
+ expect(findProjectQualitySummary().exists()).toBe(true);
+ });
+
it('sets the tab and url when a tab is clicked', async () => {
let chartsPath;
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
@@ -163,9 +171,11 @@ describe('ProjectsPipelinesChartsApp', () => {
});
});
- describe('when the dora charts are not available', () => {
+ describe('when the dora charts are not available and project quality summary is not available', () => {
beforeEach(() => {
- createComponent({ provide: { shouldRenderDoraCharts: false } });
+ createComponent({
+ provide: { shouldRenderDoraCharts: false, shouldRenderQualitySummary: false },
+ });
});
it('does not render tabs', () => {
@@ -176,4 +186,14 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findPipelineCharts().exists()).toBe(true);
});
});
+
+ describe('when the project quality summary is not available', () => {
+ beforeEach(() => {
+ createComponent({ provide: { shouldRenderQualitySummary: false } });
+ });
+
+ it('does not render the tab', () => {
+ expect(findProjectQualitySummary().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/projects/projects_filterable_list_spec.js b/spec/frontend/projects/projects_filterable_list_spec.js
index d4dbf85b5ca..a41e8b7bc09 100644
--- a/spec/frontend/projects/projects_filterable_list_spec.js
+++ b/spec/frontend/projects/projects_filterable_list_spec.js
@@ -1,5 +1,4 @@
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture, setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture } from 'helpers/fixtures';
import ProjectsFilterableList from '~/projects/projects_filterable_list';
describe('ProjectsFilterableList', () => {
@@ -15,8 +14,6 @@ describe('ProjectsFilterableList', () => {
</div>
<div class="js-projects-list-holder"></div>
`);
- // eslint-disable-next-line import/no-deprecated
- getJSONFixture('static/projects.json');
form = document.querySelector('form#project-filter-form');
filter = document.querySelector('.js-projects-list-filter');
holder = document.querySelector('.js-projects-list-holder');
diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
new file mode 100644
index 00000000000..dbea94cbd53
--- /dev/null
+++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
@@ -0,0 +1,98 @@
+import { GlTokenSelector, GlToken } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import TopicsTokenSelector from '~/projects/settings/topics/components/topics_token_selector.vue';
+
+const mockTopics = [
+ { id: 1, name: 'topic1', avatarUrl: 'avatar.com/topic1.png' },
+ { id: 2, name: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' },
+];
+
+describe('TopicsTokenSelector', () => {
+ let wrapper;
+ let div;
+ let input;
+
+ const createComponent = (selected) => {
+ wrapper = mount(TopicsTokenSelector, {
+ attachTo: div,
+ propsData: {
+ selected,
+ },
+ data() {
+ return {
+ topics: mockTopics,
+ };
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ topics: { loading: false },
+ },
+ },
+ },
+ });
+ };
+
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+
+ const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]');
+
+ const setTokenSelectorInputValue = (value) => {
+ const tokenSelectorInput = findTokenSelectorInput();
+
+ tokenSelectorInput.element.value = value;
+ tokenSelectorInput.trigger('input');
+
+ return nextTick();
+ };
+
+ const tokenSelectorTriggerEnter = (event) => {
+ const tokenSelectorInput = findTokenSelectorInput();
+ tokenSelectorInput.trigger('keydown.enter', event);
+ };
+
+ beforeEach(() => {
+ div = document.createElement('div');
+ input = document.createElement('input');
+ input.setAttribute('type', 'text');
+ input.id = 'project_topic_list_field';
+ document.body.appendChild(div);
+ document.body.appendChild(input);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ div.remove();
+ input.remove();
+ });
+
+ describe('when component is mounted', () => {
+ it('parses selected into tokens', async () => {
+ const selected = [
+ { id: 11, name: 'topic1' },
+ { id: 12, name: 'topic2' },
+ { id: 13, name: 'topic3' },
+ ];
+ createComponent(selected);
+ await nextTick();
+
+ wrapper.findAllComponents(GlToken).wrappers.forEach((tokenWrapper, index) => {
+ expect(tokenWrapper.text()).toBe(selected[index].name);
+ });
+ });
+ });
+
+ describe('when enter key is pressed', () => {
+ it('does not submit the form if token selector text input has a value', async () => {
+ createComponent();
+
+ await setTokenSelectorInputValue('topic');
+
+ const event = { preventDefault: jest.fn() };
+ tokenSelectorTriggerEnter(event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings_service_desk/components/mock_data.js b/spec/frontend/projects/settings_service_desk/components/mock_data.js
new file mode 100644
index 00000000000..934778ff601
--- /dev/null
+++ b/spec/frontend/projects/settings_service_desk/components/mock_data.js
@@ -0,0 +1,8 @@
+export const TEMPLATES = [
+ 'Project #1',
+ [
+ { name: 'Bug', project_id: 1 },
+ { name: 'Documentation', project_id: 1 },
+ { name: 'Security release', project_id: 1 },
+ ],
+];
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 8acf2376860..62224612387 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
@@ -21,6 +21,7 @@ describe('ServiceDeskRoot', () => {
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
selectedTemplate: 'Bug',
+ selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
};
@@ -52,6 +53,7 @@ describe('ServiceDeskRoot', () => {
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
initialSelectedTemplate: provideData.selectedTemplate,
+ initialSelectedFileTemplateProjectId: provideData.selectedFileTemplateProjectId,
isEnabled: provideData.initialIsEnabled,
isTemplateSaving: false,
templates: provideData.templates,
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 eacf858f22c..0fd3e7446da 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
@@ -1,4 +1,4 @@
-import { GlButton, GlFormSelect, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -13,7 +13,7 @@ describe('ServiceDeskSetting', () => {
const findIncomingEmail = () => wrapper.findByTestId('incoming-email');
const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-describer');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findTemplateDropdown = () => wrapper.find(GlFormSelect);
+ const findTemplateDropdown = () => wrapper.find(GlDropdown);
const findToggle = () => wrapper.find(GlToggle);
const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
@@ -128,6 +128,23 @@ describe('ServiceDeskSetting', () => {
expect(input.exists()).toBe(true);
expect(input.attributes('disabled')).toBeUndefined();
});
+
+ it('shows error when value contains uppercase or special chars', async () => {
+ wrapper = createComponent({
+ props: { customEmailEnabled: true },
+ mountFunction: mount,
+ });
+
+ const input = wrapper.findByTestId('project-suffix');
+
+ input.setValue('abc_A.');
+ input.trigger('blur');
+
+ await wrapper.vm.$nextTick();
+
+ const errorText = wrapper.find('.text-danger');
+ expect(errorText.exists()).toBe(true);
+ });
});
describe('customEmail is the same as incomingEmail', () => {
@@ -144,63 +161,6 @@ describe('ServiceDeskSetting', () => {
});
});
});
-
- describe('templates dropdown', () => {
- it('renders a dropdown to choose a template', () => {
- wrapper = createComponent();
-
- expect(findTemplateDropdown().exists()).toBe(true);
- });
-
- it('renders a dropdown with a default value of ""', () => {
- wrapper = createComponent({ mountFunction: mount });
-
- expect(findTemplateDropdown().element.value).toEqual('');
- });
-
- it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
- const templates = ['Bug', 'Documentation', 'Security release'];
-
- wrapper = createComponent({
- props: { initialSelectedTemplate: 'Bug', templates },
- mountFunction: mount,
- });
-
- expect(findTemplateDropdown().element.value).toEqual('Bug');
- });
-
- it('renders a dropdown with no options when the project has no templates', () => {
- wrapper = createComponent({
- props: { templates: [] },
- mountFunction: mount,
- });
-
- // The dropdown by default has one empty option
- expect(findTemplateDropdown().element.children).toHaveLength(1);
- });
-
- it('renders a dropdown with options when the project has templates', () => {
- const templates = ['Bug', 'Documentation', 'Security release'];
-
- wrapper = createComponent({
- props: { templates },
- mountFunction: mount,
- });
-
- // An empty-named template is prepended so the user can select no template
- const expectedTemplates = [''].concat(templates);
-
- const dropdown = findTemplateDropdown();
- const dropdownList = Array.from(dropdown.element.children).map(
- (option) => option.innerText,
- );
-
- expect(dropdown.element.children).toHaveLength(expectedTemplates.length);
- expect(dropdownList.includes('Bug')).toEqual(true);
- expect(dropdownList.includes('Documentation')).toEqual(true);
- expect(dropdownList.includes('Security release')).toEqual(true);
- });
- });
});
describe('save button', () => {
@@ -214,6 +174,7 @@ describe('ServiceDeskSetting', () => {
wrapper = createComponent({
props: {
initialSelectedTemplate: 'Bug',
+ initialSelectedFileTemplateProjectId: 42,
initialOutgoingName: 'GitLab Support Bot',
initialProjectKey: 'key',
},
@@ -225,6 +186,7 @@ describe('ServiceDeskSetting', () => {
const payload = {
selectedTemplate: 'Bug',
+ fileTemplateProjectId: 42,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
};
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
new file mode 100644
index 00000000000..cdb355f5a9b
--- /dev/null
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
@@ -0,0 +1,80 @@
+import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import ServiceDeskTemplateDropdown from '~/projects/settings_service_desk/components/service_desk_setting.vue';
+import { TEMPLATES } from './mock_data';
+
+describe('ServiceDeskTemplateDropdown', () => {
+ let wrapper;
+
+ const findTemplateDropdown = () => wrapper.find(GlDropdown);
+
+ const createComponent = ({ props = {} } = {}) =>
+ extendedWrapper(
+ mount(ServiceDeskTemplateDropdown, {
+ propsData: {
+ isEnabled: true,
+ ...props,
+ },
+ }),
+ );
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('templates dropdown', () => {
+ it('renders a dropdown to choose a template', () => {
+ wrapper = createComponent();
+
+ expect(findTemplateDropdown().exists()).toBe(true);
+ });
+
+ it('renders a dropdown with a default value of "Choose a template"', () => {
+ wrapper = createComponent();
+
+ expect(findTemplateDropdown().props('text')).toEqual('Choose a template');
+ });
+
+ it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
+ const templates = TEMPLATES;
+
+ wrapper = createComponent({
+ props: { initialSelectedTemplate: 'Bug', initialSelectedTemplateProjectId: 1, templates },
+ });
+
+ expect(findTemplateDropdown().props('text')).toEqual('Bug');
+ });
+
+ it('renders a dropdown with header items', () => {
+ wrapper = createComponent({
+ props: { templates: TEMPLATES },
+ });
+
+ const headerItems = wrapper.findAll(GlDropdownSectionHeader);
+
+ expect(headerItems).toHaveLength(1);
+ expect(headerItems.at(0).text()).toBe(TEMPLATES[0]);
+ });
+
+ it('renders a dropdown with options when the project has templates', () => {
+ const templates = TEMPLATES;
+
+ wrapper = createComponent({
+ props: { templates },
+ });
+
+ const expectedTemplates = templates[1];
+
+ const items = wrapper.findAll(GlDropdownItem);
+ const dropdownList = expectedTemplates.map((_, index) => items.at(index).text());
+
+ expect(items).toHaveLength(expectedTemplates.length);
+ expect(dropdownList.includes('Bug')).toEqual(true);
+ expect(dropdownList.includes('Documentation')).toEqual(true);
+ expect(dropdownList.includes('Security release')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/projects/storage_counter/components/storage_table_spec.js b/spec/frontend/projects/storage_counter/components/storage_table_spec.js
index 14298318fff..c9e56d8f033 100644
--- a/spec/frontend/projects/storage_counter/components/storage_table_spec.js
+++ b/spec/frontend/projects/storage_counter/components/storage_table_spec.js
@@ -1,4 +1,4 @@
-import { GlTable } from '@gitlab/ui';
+import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StorageTable from '~/projects/storage_counter/components/storage_table.vue';
@@ -22,7 +22,7 @@ describe('StorageTable', () => {
);
};
- const findTable = () => wrapper.findComponent(GlTable);
+ const findTable = () => wrapper.findComponent(GlTableLite);
beforeEach(() => {
createComponent();
@@ -37,6 +37,7 @@ describe('StorageTable', () => {
({ storageType: { id, name, description } }) => {
expect(wrapper.findByTestId(`${id}-name`).text()).toBe(name);
expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description);
+ expect(wrapper.findByTestId(`${id}-icon`).props('name')).toBe(id);
expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe(
defaultProvideValues.helpLinks[id.replace(`Size`, `HelpPagePath`)]
.replace(`Size`, ``)
diff --git a/spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js b/spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js
new file mode 100644
index 00000000000..01efd6f14bd
--- /dev/null
+++ b/spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js
@@ -0,0 +1,41 @@
+import { mount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import StorageTypeIcon from '~/projects/storage_counter/components/storage_type_icon.vue';
+
+describe('StorageTypeIcon', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(StorageTypeIcon, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+
+ describe('rendering icon', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ expected | provided
+ ${'doc-image'} | ${'lfsObjectsSize'}
+ ${'snippet'} | ${'snippetsSize'}
+ ${'infrastructure-registry'} | ${'repositorySize'}
+ ${'package'} | ${'packagesSize'}
+ ${'upload'} | ${'uploadsSize'}
+ ${'disk'} | ${'wikiSize'}
+ ${'disk'} | ${'anything-else'}
+ `(
+ 'renders icon with name of $expected when name prop is $provided',
+ ({ expected, provided }) => {
+ createComponent({ name: provided });
+
+ expect(findGlIcon().props('name')).toBe(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/projects/storage_counter/mock_data.js b/spec/frontend/projects/storage_counter/mock_data.js
index b9fa68b3ec7..6b3e23ac386 100644
--- a/spec/frontend/projects/storage_counter/mock_data.js
+++ b/spec/frontend/projects/storage_counter/mock_data.js
@@ -1,23 +1,6 @@
-export const mockGetProjectStorageCountGraphQLResponse = {
- data: {
- project: {
- id: 'gid://gitlab/Project/20',
- statistics: {
- buildArtifactsSize: 400000.0,
- pipelineArtifactsSize: 25000.0,
- lfsObjectsSize: 4800000.0,
- packagesSize: 3800000.0,
- repositorySize: 3900000.0,
- snippetsSize: 1200000.0,
- storageSize: 15300000.0,
- uploadsSize: 900000.0,
- wikiSize: 300000.0,
- __typename: 'ProjectStatistics',
- },
- __typename: 'Project',
- },
- },
-};
+import mockGetProjectStorageCountGraphQLResponse from 'test_fixtures/graphql/projects/storage_counter/project_storage.query.graphql.json';
+
+export { mockGetProjectStorageCountGraphQLResponse };
export const mockEmptyResponse = { data: { project: null } };
@@ -37,7 +20,7 @@ export const defaultProvideValues = {
export const projectData = {
storage: {
- totalUsage: '14.6 MiB',
+ totalUsage: '13.8 MiB',
storageTypes: [
{
storageType: {
@@ -45,7 +28,7 @@ export const projectData = {
name: 'Artifacts',
description: 'Pipeline artifacts and job artifacts, created with CI/CD.',
warningMessage:
- 'There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
+ 'Because of a known issue, the artifact total for some projects may be incorrect. For more details, read %{warningLinkStart}the epic%{warningLinkEnd}.',
helpPath: '/build-artifacts',
},
value: 400000,
@@ -53,7 +36,7 @@ export const projectData = {
{
storageType: {
id: 'lfsObjectsSize',
- name: 'LFS Storage',
+ name: 'LFS storage',
description: 'Audio samples, videos, datasets, and graphics.',
helpPath: '/lsf-objects',
},
@@ -72,7 +55,7 @@ export const projectData = {
storageType: {
id: 'repositorySize',
name: 'Repository',
- description: 'Git repository, managed by the Gitaly service.',
+ description: 'Git repository.',
helpPath: '/repository',
},
value: 3900000,
@@ -84,7 +67,7 @@ export const projectData = {
description: 'Shared bits of code and text.',
helpPath: '/snippets',
},
- value: 1200000,
+ value: 0,
},
{
storageType: {
diff --git a/spec/frontend/projects/storage_counter/utils_spec.js b/spec/frontend/projects/storage_counter/utils_spec.js
index 57c755266a0..fb91975a3cf 100644
--- a/spec/frontend/projects/storage_counter/utils_spec.js
+++ b/spec/frontend/projects/storage_counter/utils_spec.js
@@ -14,4 +14,21 @@ describe('parseGetProjectStorageResults', () => {
),
).toMatchObject(projectData);
});
+
+ it('includes storage type with size of 0 in returned value', () => {
+ const mockedResponse = mockGetProjectStorageCountGraphQLResponse.data;
+ // ensuring a specific storage type item has size of 0
+ mockedResponse.project.statistics.repositorySize = 0;
+
+ const response = parseGetProjectStorageResults(mockedResponse, defaultProvideValues.helpLinks);
+
+ expect(response.storage.storageTypes).toEqual(
+ expect.arrayContaining([
+ {
+ storageType: expect.any(Object),
+ value: 0,
+ },
+ ]),
+ );
+ });
});
diff --git a/spec/frontend/projects/upload_file_experiment_tracking_spec.js b/spec/frontend/projects/upload_file_experiment_tracking_spec.js
deleted file mode 100644
index 6817529e07e..00000000000
--- a/spec/frontend/projects/upload_file_experiment_tracking_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import ExperimentTracking from '~/experimentation/experiment_tracking';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
-
-jest.mock('~/experimentation/experiment_tracking');
-
-const eventName = 'click_upload_modal_form_submit';
-const fixture = `<a class='js-upload-file-experiment-trigger'></a><div class='project-home-panel empty-project'></div>`;
-
-beforeEach(() => {
- document.body.innerHTML = fixture;
-});
-
-afterEach(() => {
- document.body.innerHTML = '';
-});
-
-describe('trackFileUploadEvent', () => {
- it('initializes ExperimentTracking with the correct tracking event', () => {
- trackFileUploadEvent(eventName);
-
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(eventName);
- });
-
- it('calls ExperimentTracking with the correct arguments', () => {
- trackFileUploadEvent(eventName);
-
- expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', {
- label: 'blob-upload-modal',
- property: 'empty',
- });
- });
-
- it('calls ExperimentTracking with the correct arguments when the project is not empty', () => {
- document.querySelector('.empty-project').remove();
-
- trackFileUploadEvent(eventName);
-
- expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', {
- label: 'blob-upload-modal',
- property: 'nonempty',
- });
- });
-});
diff --git a/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
index 67f62815720..486fb699275 100644
--- a/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
@@ -66,8 +66,8 @@ describe('RelatedMergeRequests', () => {
describe('template', () => {
it('should render related merge request items', () => {
- expect(wrapper.find('.js-items-count').text()).toEqual('2');
- expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2);
+ expect(wrapper.find('[data-testid="count"]').text()).toBe('2');
+ expect(wrapper.findAll(RelatedIssuableItem)).toHaveLength(2);
const props = wrapper.findAll(RelatedIssuableItem).at(1).props();
const data = mockData[1];
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index 114e46ce64b..0f416e46dba 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,6 +1,7 @@
import { GlDropdownItem } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
+import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new';
@@ -84,7 +85,8 @@ describe('releases/components/tag_field_new', () => {
beforeEach(() => createComponent());
it('renders a label', () => {
- expect(findTagNameFormGroup().attributes().label).toBe('Tag name');
+ expect(findTagNameFormGroup().attributes().label).toBe(__('Tag name'));
+ expect(findTagNameFormGroup().props().labelDescription).toBe(__('*Required'));
});
describe('when the user selects a new tag name', () => {
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 59db537282b..d40e97bf5a3 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -1,5 +1,5 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -19,6 +19,15 @@ import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import { isLoggedIn } from '~/lib/utils/common_utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ simpleViewerMock,
+ richViewerMock,
+ projectMock,
+ userPermissionsMock,
+ propsMock,
+ refMock,
+} from '../mock_data';
jest.mock('~/repository/components/blob_viewers');
jest.mock('~/lib/utils/url_utility');
@@ -27,151 +36,63 @@ jest.mock('~/lib/utils/common_utils');
let wrapper;
let mockResolver;
-const simpleMockData = {
- name: 'some_file.js',
- size: 123,
- rawSize: 123,
- rawTextBlob: 'raw content',
- type: 'text',
- fileType: 'text',
- tooLarge: false,
- path: 'some_file.js',
- webPath: 'some_file.js',
- editBlobPath: 'some_file.js/edit',
- ideEditPath: 'some_file.js/ide/edit',
- forkAndEditPath: 'some_file.js/fork/edit',
- ideForkAndEditPath: 'some_file.js/fork/ide',
- canModifyBlob: true,
- storedExternally: false,
- rawPath: 'some_file.js',
- externalStorageUrl: 'some_file.js',
- replacePath: 'some_file.js/replace',
- deletePath: 'some_file.js/delete',
- simpleViewer: {
- fileType: 'text',
- tooLarge: false,
- type: 'simple',
- renderError: null,
- },
- richViewer: null,
-};
-const richMockData = {
- ...simpleMockData,
- richViewer: {
- fileType: 'markup',
- tooLarge: false,
- type: 'rich',
- renderError: null,
- },
-};
-
-const projectMockData = {
- userPermissions: {
- pushCode: true,
- downloadCode: true,
- createMergeRequestIn: true,
- forkProject: true,
- },
- repository: {
- empty: false,
- },
-};
-
const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
-const createComponentWithApollo = (mockData = {}, inject = {}) => {
+const createComponent = async (mockData = {}, mountFn = shallowMount) => {
localVue.use(VueApollo);
- const defaultPushCode = projectMockData.userPermissions.pushCode;
- const defaultDownloadCode = projectMockData.userPermissions.downloadCode;
- const defaultEmptyRepo = projectMockData.repository.empty;
const {
- blobs,
- emptyRepo = defaultEmptyRepo,
- canPushCode = defaultPushCode,
- canDownloadCode = defaultDownloadCode,
- createMergeRequestIn = projectMockData.userPermissions.createMergeRequestIn,
- forkProject = projectMockData.userPermissions.forkProject,
- pathLocks = [],
+ blob = simpleViewerMock,
+ empty = projectMock.repository.empty,
+ pushCode = userPermissionsMock.pushCode,
+ forkProject = userPermissionsMock.forkProject,
+ downloadCode = userPermissionsMock.downloadCode,
+ createMergeRequestIn = userPermissionsMock.createMergeRequestIn,
+ isBinary,
+ inject = {},
} = mockData;
- mockResolver = jest.fn().mockResolvedValue({
- data: {
- project: {
- id: '1234',
- userPermissions: {
- pushCode: canPushCode,
- downloadCode: canDownloadCode,
- createMergeRequestIn,
- forkProject,
- },
- pathLocks: {
- nodes: pathLocks,
- },
- repository: {
- empty: emptyRepo,
- blobs: {
- nodes: [blobs],
- },
- },
- },
+ const project = {
+ ...projectMock,
+ userPermissions: {
+ pushCode,
+ forkProject,
+ downloadCode,
+ createMergeRequestIn,
+ },
+ repository: {
+ empty,
+ blobs: { nodes: [blob] },
},
+ };
+
+ mockResolver = jest.fn().mockResolvedValue({
+ data: { isBinary, project },
});
const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
- wrapper = shallowMount(BlobContentViewer, {
- localVue,
- apolloProvider: fakeApollo,
- propsData: {
- path: 'some_file.js',
- projectPath: 'some/path',
- },
- mixins: [
- {
- data: () => ({ ref: 'default-ref' }),
- },
- ],
- provide: {
- ...inject,
- },
- });
-};
+ wrapper = extendedWrapper(
+ mountFn(BlobContentViewer, {
+ localVue,
+ apolloProvider: fakeApollo,
+ propsData: propsMock,
+ mixins: [{ data: () => ({ ref: refMock }) }],
+ provide: { ...inject },
+ }),
+ );
-const createFactory = (mountFn) => (
- { props = {}, mockData = {}, stubs = {} } = {},
- loading = false,
-) => {
- wrapper = mountFn(BlobContentViewer, {
- propsData: {
- path: 'some_file.js',
- projectPath: 'some/path',
- ...props,
- },
- mocks: {
- $apollo: {
- queries: {
- project: {
- loading,
- refetch: jest.fn(),
- },
- },
- },
- },
- stubs,
- });
+ wrapper.setData({ project, isBinary });
- wrapper.setData(mockData);
+ await waitForPromises();
};
-const factory = createFactory(shallowMount);
-const fullFactory = createFactory(mount);
-
describe('Blob content viewer component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
+ const findPipelineEditor = () => wrapper.findByTestId('pipeline-editor');
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
@@ -187,25 +108,24 @@ describe('Blob content viewer component', () => {
});
it('renders a GlLoadingIcon component', () => {
- factory({ mockData: { blobInfo: simpleMockData } }, true);
+ createComponent();
expect(findLoadingIcon().exists()).toBe(true);
});
describe('simple viewer', () => {
- beforeEach(() => {
- factory({ mockData: { blobInfo: simpleMockData } });
- });
+ it('renders a BlobHeader component', async () => {
+ await createComponent();
- it('renders a BlobHeader component', () => {
expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
- expect(findBlobHeader().props('blob')).toEqual(simpleMockData);
+ expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
});
- it('renders a BlobContent component', () => {
- expect(findBlobContent().props('loading')).toEqual(false);
+ it('renders a BlobContent component', async () => {
+ await createComponent();
+
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text',
@@ -217,8 +137,7 @@ describe('Blob content viewer component', () => {
describe('legacy viewers', () => {
it('loads a legacy viewer when a viewer component is not available', async () => {
- createComponentWithApollo({ blobs: { ...simpleMockData, fileType: 'unknown' } });
- await waitForPromises();
+ await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } });
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
@@ -227,21 +146,18 @@ describe('Blob content viewer component', () => {
});
describe('rich viewer', () => {
- beforeEach(() => {
- factory({
- mockData: { blobInfo: richMockData, activeViewerType: 'rich' },
- });
- });
+ it('renders a BlobHeader component', async () => {
+ await createComponent({ blob: richViewerMock });
- it('renders a BlobHeader component', () => {
expect(findBlobHeader().props('activeViewerType')).toEqual('rich');
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
- expect(findBlobHeader().props('blob')).toEqual(richMockData);
+ expect(findBlobHeader().props('blob')).toEqual(richViewerMock);
});
- it('renders a BlobContent component', () => {
- expect(findBlobContent().props('loading')).toEqual(false);
+ it('renders a BlobContent component', async () => {
+ await createComponent({ blob: richViewerMock });
+
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
@@ -252,6 +168,8 @@ describe('Blob content viewer component', () => {
});
it('updates viewer type when viewer changed is clicked', async () => {
+ await createComponent({ blob: richViewerMock });
+
expect(findBlobContent().props('activeViewer')).toEqual(
expect.objectContaining({
type: 'rich',
@@ -273,8 +191,7 @@ describe('Blob content viewer component', () => {
describe('legacy viewers', () => {
it('loads a legacy viewer when a viewer component is not available', async () => {
- createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } });
- await waitForPromises();
+ await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } });
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich');
@@ -287,9 +204,9 @@ describe('Blob content viewer component', () => {
viewerProps.mockRestore();
});
- it('does not render a BlobContent component if a Blob viewer is available', () => {
- loadViewer.mockReturnValueOnce(() => true);
- factory({ mockData: { blobInfo: richMockData } });
+ it('does not render a BlobContent component if a Blob viewer is available', async () => {
+ loadViewer.mockReturnValue(() => true);
+ await createComponent({ blob: richViewerMock });
expect(findBlobContent().exists()).toBe(false);
});
@@ -305,15 +222,13 @@ describe('Blob content viewer component', () => {
loadViewer.mockReturnValue(loadViewerReturnValue);
viewerProps.mockReturnValue(viewerPropsReturnValue);
- factory({
- mockData: {
- blobInfo: {
- ...simpleMockData,
- fileType: null,
- simpleViewer: {
- ...simpleMockData.simpleViewer,
- fileType: viewer,
- },
+ createComponent({
+ blob: {
+ ...simpleViewerMock,
+ fileType: 'null',
+ simpleViewer: {
+ ...simpleViewerMock.simpleViewer,
+ fileType: viewer,
},
},
});
@@ -327,18 +242,10 @@ describe('Blob content viewer component', () => {
});
describe('BlobHeader action slot', () => {
- const { ideEditPath, editBlobPath } = simpleMockData;
+ const { ideEditPath, editBlobPath } = simpleViewerMock;
it('renders BlobHeaderEdit buttons in simple viewer', async () => {
- fullFactory({
- mockData: { blobInfo: simpleMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
@@ -348,15 +255,7 @@ describe('Blob content viewer component', () => {
});
it('renders BlobHeaderEdit button in rich viewer', async () => {
- fullFactory({
- mockData: { blobInfo: richMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent({ blob: richViewerMock }, mount);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
@@ -366,15 +265,7 @@ describe('Blob content viewer component', () => {
});
it('renders BlobHeaderEdit button for binary files', async () => {
- fullFactory({
- mockData: { blobInfo: richMockData, isBinary: true },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent({ blob: richViewerMock, isBinary: true }, mount);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
@@ -383,42 +274,36 @@ describe('Blob content viewer component', () => {
});
});
- describe('blob header binary file', () => {
- it.each([richMockData, { simpleViewer: { fileType: 'download' } }])(
- 'passes the correct isBinary value when viewing a binary file',
- async (blobInfo) => {
- fullFactory({
- mockData: {
- blobInfo,
- isBinary: true,
- },
- stubs: { BlobContent: true, BlobReplace: true },
- });
+ it('renders Pipeline Editor button for .gitlab-ci files', async () => {
+ const pipelineEditorPath = 'some/path/.gitlab-ce';
+ const blob = { ...simpleViewerMock, pipelineEditorPath };
+ await createComponent({ blob, inject: { BlobContent: true, BlobReplace: true } }, mount);
- await nextTick();
+ expect(findPipelineEditor().exists()).toBe(true);
+ expect(findPipelineEditor().attributes('href')).toBe(pipelineEditorPath);
+ });
- expect(findBlobHeader().props('isBinary')).toBe(true);
- },
- );
+ describe('blob header binary file', () => {
+ it('passes the correct isBinary value when viewing a binary file', async () => {
+ await createComponent({ blob: richViewerMock, isBinary: true });
+
+ expect(findBlobHeader().props('isBinary')).toBe(true);
+ });
it('passes the correct header props when viewing a non-text file', async () => {
- fullFactory({
- mockData: {
- blobInfo: {
- ...simpleMockData,
+ await createComponent(
+ {
+ blob: {
+ ...simpleViewerMock,
simpleViewer: {
- ...simpleMockData.simpleViewer,
+ ...simpleViewerMock.simpleViewer,
fileType: 'image',
},
},
+ isBinary: true,
},
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ mount,
+ );
expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
expect(findBlobHeader().props('isBinary')).toBe(true);
@@ -427,27 +312,16 @@ describe('Blob content viewer component', () => {
});
describe('BlobButtonGroup', () => {
- const { name, path, replacePath, webPath } = simpleMockData;
+ const { name, path, replacePath, webPath } = simpleViewerMock;
const {
userPermissions: { pushCode, downloadCode },
repository: { empty },
- } = projectMockData;
+ } = projectMock;
it('renders component', async () => {
window.gon.current_user_id = 1;
- fullFactory({
- mockData: {
- blobInfo: simpleMockData,
- project: { userPermissions: { pushCode, downloadCode }, repository: { empty } },
- },
- stubs: {
- BlobContent: true,
- BlobButtonGroup: true,
- },
- });
-
- await nextTick();
+ await createComponent({ pushCode, downloadCode, empty }, mount);
expect(findBlobButtonGroup().props()).toMatchObject({
name,
@@ -467,21 +341,14 @@ describe('Blob content viewer component', () => {
${false} | ${true} | ${false}
${true} | ${false} | ${false}
`('passes the correct lock states', async ({ canPushCode, canDownloadCode, canLock }) => {
- fullFactory({
- mockData: {
- blobInfo: simpleMockData,
- project: {
- userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode },
- repository: { empty },
- },
+ await createComponent(
+ {
+ pushCode: canPushCode,
+ downloadCode: canDownloadCode,
+ empty,
},
- stubs: {
- BlobContent: true,
- BlobButtonGroup: true,
- },
- });
-
- await nextTick();
+ mount,
+ );
expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
});
@@ -489,15 +356,7 @@ describe('Blob content viewer component', () => {
it('does not render if not logged in', async () => {
isLoggedIn.mockReturnValueOnce(false);
- fullFactory({
- mockData: { blobInfo: simpleMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent();
expect(findBlobButtonGroup().exists()).toBe(false);
});
@@ -506,10 +365,7 @@ describe('Blob content viewer component', () => {
describe('blob info query', () => {
it('is called with originalBranch value if the prop has a value', async () => {
- const inject = { originalBranch: 'some-branch' };
- createComponentWithApollo({ blobs: simpleMockData }, inject);
-
- await waitForPromises();
+ await createComponent({ inject: { originalBranch: 'some-branch' } });
expect(mockResolver).toHaveBeenCalledWith(
expect.objectContaining({
@@ -519,10 +375,7 @@ describe('Blob content viewer component', () => {
});
it('is called with ref value if the originalBranch prop has no value', async () => {
- const inject = { originalBranch: null };
- createComponentWithApollo({ blobs: simpleMockData }, inject);
-
- await waitForPromises();
+ await createComponent();
expect(mockResolver).toHaveBeenCalledWith(
expect.objectContaining({
@@ -533,24 +386,16 @@ describe('Blob content viewer component', () => {
});
describe('edit blob', () => {
- beforeEach(() => {
- fullFactory({
- mockData: { blobInfo: simpleMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
- });
+ beforeEach(() => createComponent({}, mount));
it('simple edit redirects to the simple editor', () => {
findBlobEdit().vm.$emit('edit', 'simple');
- expect(redirectTo).toHaveBeenCalledWith(simpleMockData.editBlobPath);
+ expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
});
it('IDE edit redirects to the IDE editor', () => {
findBlobEdit().vm.$emit('edit', 'ide');
- expect(redirectTo).toHaveBeenCalledWith(simpleMockData.ideEditPath);
+ expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
});
it.each`
@@ -569,16 +414,14 @@ describe('Blob content viewer component', () => {
showForkSuggestion,
}) => {
isLoggedIn.mockReturnValueOnce(loggedIn);
- fullFactory({
- mockData: {
- blobInfo: { ...simpleMockData, canModifyBlob },
- project: { userPermissions: { createMergeRequestIn, forkProject } },
+ await createComponent(
+ {
+ blob: { ...simpleViewerMock, canModifyBlob },
+ createMergeRequestIn,
+ forkProject,
},
- stubs: {
- BlobContent: true,
- BlobButtonGroup: true,
- },
- });
+ mount,
+ );
findBlobEdit().vm.$emit('edit', 'simple');
await nextTick();
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index 08a6583b60c..36847107558 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -6,11 +6,9 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-jest.mock('~/projects/upload_file_experiment_tracking');
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -162,10 +160,6 @@ describe('UploadBlobModal', () => {
await waitForPromises();
});
- it('tracks the click_upload_modal_trigger event when opening the modal', () => {
- expect(trackFileUploadEvent).toHaveBeenCalledWith('click_upload_modal_form_submit');
- });
-
it('redirects to the uploaded file', () => {
expect(visitUrl).toHaveBeenCalled();
});
@@ -185,10 +179,6 @@ describe('UploadBlobModal', () => {
await waitForPromises();
});
- it('does not track an event', () => {
- expect(trackFileUploadEvent).not.toHaveBeenCalled();
- });
-
it('creates a flash error', () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Error uploading file. Please try again.',
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
new file mode 100644
index 00000000000..adf5991ac3c
--- /dev/null
+++ b/spec/frontend/repository/mock_data.js
@@ -0,0 +1,57 @@
+export const simpleViewerMock = {
+ name: 'some_file.js',
+ size: 123,
+ rawSize: 123,
+ rawTextBlob: 'raw content',
+ fileType: 'text',
+ path: 'some_file.js',
+ webPath: 'some_file.js',
+ editBlobPath: 'some_file.js/edit',
+ ideEditPath: 'some_file.js/ide/edit',
+ forkAndEditPath: 'some_file.js/fork/edit',
+ ideForkAndEditPath: 'some_file.js/fork/ide',
+ canModifyBlob: true,
+ storedExternally: false,
+ rawPath: 'some_file.js',
+ replacePath: 'some_file.js/replace',
+ pipelineEditorPath: '',
+ simpleViewer: {
+ fileType: 'text',
+ tooLarge: false,
+ type: 'simple',
+ renderError: null,
+ },
+ richViewer: null,
+};
+
+export const richViewerMock = {
+ ...simpleViewerMock,
+ richViewer: {
+ fileType: 'markup',
+ tooLarge: false,
+ type: 'rich',
+ renderError: null,
+ },
+};
+
+export const userPermissionsMock = {
+ pushCode: true,
+ forkProject: true,
+ downloadCode: true,
+ createMergeRequestIn: true,
+};
+
+export const projectMock = {
+ id: '1234',
+ userPermissions: userPermissionsMock,
+ pathLocks: {
+ nodes: [],
+ },
+ repository: {
+ empty: false,
+ },
+};
+
+export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
+
+export const refMock = 'default-ref';
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 33e9c122080..7eda9aa2850 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -10,9 +10,10 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
+import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import {
@@ -22,7 +23,6 @@ import {
DEFAULT_SORT,
INSTANCE_TYPE,
PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
@@ -34,7 +34,11 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockActiveRunnersCount = 2;
+const mockActiveRunnersCount = '2';
+const mockAllRunnersCount = '6';
+const mockInstanceRunnersCount = '3';
+const mockGroupRunnersCount = '2';
+const mockProjectRunnersCount = '1';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -50,7 +54,8 @@ describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
- const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
+ const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
@@ -66,8 +71,12 @@ describe('AdminRunnersApp', () => {
localVue,
apolloProvider: createMockApollo(handlers),
propsData: {
- activeRunnersCount: mockActiveRunnersCount,
registrationToken: mockRegistrationToken,
+ activeRunnersCount: mockActiveRunnersCount,
+ allRunnersCount: mockAllRunnersCount,
+ instanceRunnersCount: mockInstanceRunnersCount,
+ groupRunnersCount: mockGroupRunnersCount,
+ projectRunnersCount: mockProjectRunnersCount,
...props,
},
});
@@ -86,8 +95,19 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
+ it('shows the runner tabs with a runner count', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
+ `All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`,
+ );
+ });
+
it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);
});
it('shows the runners list', () => {
@@ -126,10 +146,6 @@ describe('AdminRunnersApp', () => {
options: expect.any(Array),
}),
expect.objectContaining({
- type: PARAM_KEY_RUNNER_TYPE,
- options: expect.any(Array),
- }),
- expect.objectContaining({
type: PARAM_KEY_TAG,
recentTokenValuesStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
}),
@@ -154,9 +170,9 @@ describe('AdminRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
+ runnerType: INSTANCE_TYPE,
filters: [
{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
- { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
{ type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
@@ -178,6 +194,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is selected by the user', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
+ runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 5aa3879ac3e..2874bdbe280 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -8,12 +8,11 @@ import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue'
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
-import { runnersData, runnerData } from '../../mock_data';
+import { runnersData } from '../../mock_data';
const mockRunner = runnersData.data.runners.nodes[0];
-const mockRunnerDetails = runnerData.data.runner;
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value;
@@ -27,7 +26,7 @@ jest.mock('~/runner/sentry_utils');
describe('RunnerTypeCell', () => {
let wrapper;
const runnerDeleteMutationHandler = jest.fn();
- const runnerUpdateMutationHandler = jest.fn();
+ const runnerActionsUpdateMutationHandler = jest.fn();
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
@@ -46,7 +45,7 @@ describe('RunnerTypeCell', () => {
localVue,
apolloProvider: createMockApollo([
[runnerDeleteMutation, runnerDeleteMutationHandler],
- [runnerUpdateMutation, runnerUpdateMutationHandler],
+ [runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler],
]),
...options,
}),
@@ -62,10 +61,10 @@ describe('RunnerTypeCell', () => {
},
});
- runnerUpdateMutationHandler.mockResolvedValue({
+ runnerActionsUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
- runner: mockRunnerDetails,
+ runner: mockRunner,
errors: [],
},
},
@@ -74,7 +73,7 @@ describe('RunnerTypeCell', () => {
afterEach(() => {
runnerDeleteMutationHandler.mockReset();
- runnerUpdateMutationHandler.mockReset();
+ runnerActionsUpdateMutationHandler.mockReset();
wrapper.destroy();
});
@@ -116,12 +115,12 @@ describe('RunnerTypeCell', () => {
describe(`When clicking on the ${icon} button`, () => {
it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
- expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(0);
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0);
await findToggleActiveBtn().vm.$emit('click');
- expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(1);
- expect(runnerUpdateMutationHandler).toHaveBeenCalledWith({
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1);
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({
input: {
id: mockRunner.id,
active: newActiveValue,
@@ -145,7 +144,7 @@ describe('RunnerTypeCell', () => {
const mockErrorMsg = 'Update error!';
beforeEach(async () => {
- runnerUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+ runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
await findToggleActiveBtn().vm.$emit('click');
});
@@ -167,10 +166,10 @@ describe('RunnerTypeCell', () => {
const mockErrorMsg2 = 'User not allowed!';
beforeEach(async () => {
- runnerUpdateMutationHandler.mockResolvedValue({
+ runnerActionsUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
- runner: runnerData.data.runner,
+ runner: mockRunner,
errors: [mockErrorMsg, mockErrorMsg2],
},
},
diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
new file mode 100644
index 00000000000..20a1cdf7236
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
@@ -0,0 +1,69 @@
+import { GlBadge } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
+import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
+
+describe('RunnerTypeCell', () => {
+ let wrapper;
+
+ const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i);
+
+ const createComponent = ({ runner = {} } = {}) => {
+ wrapper = mount(RunnerStatusCell, {
+ propsData: {
+ runner: {
+ runnerType: INSTANCE_TYPE,
+ active: true,
+ status: STATUS_ONLINE,
+ ...runner,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays online status', () => {
+ createComponent();
+
+ expect(wrapper.text()).toMatchInterpolatedText('online');
+ expect(findBadgeAt(0).text()).toBe('online');
+ });
+
+ it('Displays offline status', () => {
+ createComponent({
+ runner: {
+ status: STATUS_OFFLINE,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText('offline');
+ expect(findBadgeAt(0).text()).toBe('offline');
+ });
+
+ it('Displays paused status', () => {
+ createComponent({
+ runner: {
+ active: false,
+ status: STATUS_ONLINE,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText('online paused');
+
+ expect(findBadgeAt(0).text()).toBe('online');
+ expect(findBadgeAt(1).text()).toBe('paused');
+ });
+
+ it('Is empty when data is missing', () => {
+ createComponent({
+ runner: {
+ status: null,
+ },
+ });
+
+ expect(wrapper.text()).toBe('');
+ });
+});
diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
index 1c9282e0acd..b6d957d27ea 100644
--- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
@@ -1,5 +1,6 @@
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
+import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockId = '1';
const mockShortSha = '2P6oDVDm';
@@ -8,13 +9,17 @@ const mockDescription = 'runner-1';
describe('RunnerTypeCell', () => {
let wrapper;
- const createComponent = (options) => {
- wrapper = mount(RunnerSummaryCell, {
+ const findLockIcon = () => wrapper.findByTestId('lock-icon');
+
+ const createComponent = (runner, options) => {
+ wrapper = mountExtended(RunnerSummaryCell, {
propsData: {
runner: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
shortSha: mockShortSha,
description: mockDescription,
+ runnerType: INSTANCE_TYPE,
+ ...runner,
},
},
...options,
@@ -33,6 +38,23 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`);
});
+ it('Displays the runner type', () => {
+ expect(wrapper.text()).toContain('shared');
+ });
+
+ it('Does not display the locked icon', () => {
+ expect(findLockIcon().exists()).toBe(false);
+ });
+
+ it('Displays the locked icon for locked runners', () => {
+ createComponent({
+ runnerType: PROJECT_TYPE,
+ locked: true,
+ });
+
+ expect(findLockIcon().exists()).toBe(true);
+ });
+
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockDescription);
});
@@ -40,11 +62,14 @@ describe('RunnerTypeCell', () => {
it('Displays a custom slot', () => {
const slotContent = 'My custom runner summary';
- createComponent({
- slots: {
- 'runner-name': slotContent,
+ createComponent(
+ {},
+ {
+ slots: {
+ 'runner-name': slotContent,
+ },
},
- });
+ );
expect(wrapper.text()).toContain(slotContent);
});
diff --git a/spec/frontend/runner/components/cells/runner_type_cell_spec.js b/spec/frontend/runner/components/cells/runner_type_cell_spec.js
deleted file mode 100644
index 48958a282fc..00000000000
--- a/spec/frontend/runner/components/cells/runner_type_cell_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import RunnerTypeCell from '~/runner/components/cells/runner_type_cell.vue';
-import { INSTANCE_TYPE } from '~/runner/constants';
-
-describe('RunnerTypeCell', () => {
- let wrapper;
-
- const findBadges = () => wrapper.findAllComponents(GlBadge);
-
- const createComponent = ({ runner = {} } = {}) => {
- wrapper = mount(RunnerTypeCell, {
- propsData: {
- runner: {
- runnerType: INSTANCE_TYPE,
- active: true,
- locked: false,
- ...runner,
- },
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays the runner type', () => {
- createComponent();
-
- expect(findBadges()).toHaveLength(1);
- expect(findBadges().at(0).text()).toBe('shared');
- });
-
- it('Displays locked and paused states', () => {
- createComponent({
- runner: {
- active: false,
- locked: true,
- },
- });
-
- expect(findBadges()).toHaveLength(3);
- expect(findBadges().at(0).text()).toBe('shared');
- expect(findBadges().at(1).text()).toBe('locked');
- expect(findBadges().at(2).text()).toBe('paused');
- });
-});
diff --git a/spec/frontend/runner/components/helpers/masked_value_spec.js b/spec/frontend/runner/components/helpers/masked_value_spec.js
deleted file mode 100644
index f87315057ec..00000000000
--- a/spec/frontend/runner/components/helpers/masked_value_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-
-const mockSecret = '01234567890';
-const mockMasked = '***********';
-
-describe('MaskedValue', () => {
- let wrapper;
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(MaskedValue, {
- propsData: {
- value: mockSecret,
- ...props,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays masked value by default', () => {
- expect(wrapper.text()).toBe(mockMasked);
- });
-
- describe('When the icon is clicked', () => {
- beforeEach(() => {
- findButton().vm.$emit('click');
- });
-
- it('Displays the actual value', () => {
- expect(wrapper.text()).toBe(mockSecret);
- expect(wrapper.text()).not.toBe(mockMasked);
- });
-
- it('When user clicks again, displays masked value', async () => {
- await findButton().vm.$emit('click');
-
- expect(wrapper.text()).toBe(mockMasked);
- expect(wrapper.text()).not.toBe(mockSecret);
- });
- });
-});
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
new file mode 100644
index 00000000000..d18d2bec18e
--- /dev/null
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -0,0 +1,169 @@
+import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount, createWrapper } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
+import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
+
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+
+import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+
+import {
+ mockGraphqlRunnerPlatforms,
+ mockGraphqlInstructions,
+} from 'jest/vue_shared/components/runner_instructions/mock_data';
+
+const mockToken = '0123456789';
+const maskToken = '**********';
+
+describe('RegistrationDropdown', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+
+ const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
+ const findTokenResetDropdownItem = () =>
+ wrapper.findComponent(RegistrationTokenResetDropdownItem);
+
+ const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
+
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
+ wrapper = extendedWrapper(
+ mountFn(RegistrationDropdown, {
+ propsData: {
+ registrationToken: mockToken,
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ ...options,
+ }),
+ );
+ };
+
+ it.each`
+ type | text
+ ${INSTANCE_TYPE} | ${'Register an instance runner'}
+ ${GROUP_TYPE} | ${'Register a group runner'}
+ ${PROJECT_TYPE} | ${'Register a project runner'}
+ `('Dropdown text for type $type is "$text"', () => {
+ createComponent({ props: { type: INSTANCE_TYPE } }, mount);
+
+ expect(wrapper.text()).toContain('Register an instance runner');
+ });
+
+ it('Passes attributes to the dropdown component', () => {
+ createComponent({ attrs: { right: true } });
+
+ expect(findDropdown().attributes()).toMatchObject({ right: 'true' });
+ });
+
+ describe('Instructions dropdown item', () => {
+ it('Displays "Show runner" dropdown item', () => {
+ createComponent();
+
+ expect(findRegistrationInstructionsDropdownItem().text()).toBe(
+ 'Show runner installation and registration instructions',
+ );
+ });
+
+ describe('When the dropdown item is clicked', () => {
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ const findModalInBody = () =>
+ createWrapper(document.body).find('[data-testid="runner-instructions-modal"]');
+
+ beforeEach(() => {
+ createComponent(
+ {
+ localVue,
+ // Mock load modal contents from API
+ apolloProvider: createMockApollo(requestHandlers),
+ // Use `attachTo` to find the modal
+ attachTo: document.body,
+ },
+ mount,
+ );
+
+ findRegistrationInstructionsDropdownItem().trigger('click');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('opens the modal with contents', () => {
+ const modalText = findModalInBody()
+ .text()
+ .replace(/[\n\t\s]+/g, ' ');
+
+ expect(modalText).toContain('Install a runner');
+
+ // Environment selector
+ expect(modalText).toContain('Environment');
+ expect(modalText).toContain('Linux macOS Windows Docker Kubernetes');
+
+ // Architecture selector
+ expect(modalText).toContain('Architecture');
+ expect(modalText).toContain('amd64 amd64 386 arm arm64');
+
+ expect(modalText).toContain('Download and install binary');
+ });
+ });
+ });
+
+ describe('Registration token', () => {
+ it('Displays dropdown form for the registration token', () => {
+ createComponent();
+
+ expect(findTokenDropdownItem().exists()).toBe(true);
+ });
+
+ it('Displays masked value by default', () => {
+ createComponent({}, mount);
+
+ expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
+ `Registration token ${maskToken}`,
+ );
+ });
+ });
+
+ describe('Reset token item', () => {
+ it('Displays registration token reset item', () => {
+ createComponent();
+
+ expect(findTokenResetDropdownItem().exists()).toBe(true);
+ });
+
+ it.each([INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE])('Set up token reset for %s', (type) => {
+ createComponent({ props: { type } });
+
+ expect(findTokenResetDropdownItem().props('type')).toBe(type);
+ });
+ });
+
+ it('Updates the token when it gets reset', async () => {
+ createComponent({}, mount);
+
+ const newToken = 'mock1';
+
+ findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
+ findToggleMaskButton().vm.$emit('click', { stopPropagation: jest.fn() });
+ await nextTick();
+
+ expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
+ `Registration token ${newToken}`,
+ );
+ });
+});
diff --git a/spec/frontend/runner/components/runner_registration_token_reset_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 8b360b88417..0d002c272b4 100644
--- a/spec/frontend/runner/components/runner_registration_token_reset_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,11 +1,11 @@
-import { GlButton } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash, { FLASH_TYPES } from '~/flash';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
+import createFlash from '~/flash';
+import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
@@ -15,17 +15,20 @@ jest.mock('~/runner/sentry_utils');
const localVue = createLocalVue();
localVue.use(VueApollo);
+localVue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
-describe('RunnerRegistrationTokenReset', () => {
+describe('RegistrationTokenResetDropdownItem', () => {
let wrapper;
let runnersRegistrationTokenResetMutationHandler;
+ let showToast;
- const findButton = () => wrapper.findComponent(GlButton);
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createComponent = ({ props, provide = {} } = {}) => {
- wrapper = shallowMount(RunnerRegistrationTokenReset, {
+ wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
localVue,
provide,
propsData: {
@@ -36,6 +39,8 @@ describe('RunnerRegistrationTokenReset', () => {
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
});
+
+ showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
beforeEach(() => {
@@ -58,7 +63,7 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('Displays reset button', () => {
- expect(findButton().exists()).toBe(true);
+ expect(findDropdownItem().exists()).toBe(true);
});
describe('On click and confirmation', () => {
@@ -78,7 +83,8 @@ describe('RunnerRegistrationTokenReset', () => {
});
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+
+ findDropdownItem().trigger('click');
await waitForPromises();
});
@@ -95,14 +101,13 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('does not show a loading state', () => {
- expect(findButton().props('loading')).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('shows confirmation', () => {
- expect(createFlash).toHaveBeenLastCalledWith({
- message: expect.stringContaining('registration token generated'),
- type: FLASH_TYPES.SUCCESS,
- });
+ expect(showToast).toHaveBeenLastCalledWith(
+ expect.stringContaining('registration token generated'),
+ );
});
});
});
@@ -110,7 +115,7 @@ describe('RunnerRegistrationTokenReset', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
window.confirm.mockReturnValueOnce(false);
- findButton().vm.$emit('click');
+ findDropdownItem().vm.$emit('click');
await waitForPromises();
});
@@ -123,11 +128,11 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('does not show a loading state', () => {
- expect(findButton().props('loading')).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('does not shows confirmation', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(showToast).not.toHaveBeenCalled();
});
});
@@ -138,7 +143,7 @@ describe('RunnerRegistrationTokenReset', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -164,7 +169,7 @@ describe('RunnerRegistrationTokenReset', () => {
});
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -180,10 +185,10 @@ describe('RunnerRegistrationTokenReset', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
window.confirm.mockReturnValue(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await nextTick();
- expect(findButton().props('loading')).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
new file mode 100644
index 00000000000..f53ae165344
--- /dev/null
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -0,0 +1,109 @@
+import { nextTick } from 'vue';
+import { GlToast } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import RegistrationToken from '~/runner/components/registration/registration_token.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+const mockToken = '01234567890';
+const mockMasked = '***********';
+
+describe('RegistrationToken', () => {
+ let wrapper;
+ let stopPropagation;
+ let showToast;
+
+ const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
+ const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
+
+ const vueWithGlToast = () => {
+ const localVue = createLocalVue();
+ localVue.use(GlToast);
+ return localVue;
+ };
+
+ const createComponent = ({ props = {}, withGlToast = true } = {}) => {
+ const localVue = withGlToast ? vueWithGlToast() : undefined;
+
+ wrapper = extendedWrapper(
+ shallowMount(RegistrationToken, {
+ propsData: {
+ value: mockToken,
+ ...props,
+ },
+ localVue,
+ }),
+ );
+
+ showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
+ };
+
+ beforeEach(() => {
+ stopPropagation = jest.fn();
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays masked value by default', () => {
+ expect(wrapper.text()).toBe(mockMasked);
+ });
+
+ it('Displays button to reveal token', () => {
+ expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
+ });
+
+ it('Can copy the original token value', () => {
+ expect(findCopyButton().props('text')).toBe(mockToken);
+ });
+
+ describe('When the reveal icon is clicked', () => {
+ beforeEach(() => {
+ findToggleMaskButton().vm.$emit('click', { stopPropagation });
+ });
+
+ it('Click event is not propagated', async () => {
+ expect(stopPropagation).toHaveBeenCalledTimes(1);
+ });
+
+ it('Displays the actual value', () => {
+ expect(wrapper.text()).toBe(mockToken);
+ });
+
+ it('Can copy the original token value', () => {
+ expect(findCopyButton().props('text')).toBe(mockToken);
+ });
+
+ it('Displays button to mask token', () => {
+ expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to hide');
+ });
+
+ it('When user clicks again, displays masked value', async () => {
+ findToggleMaskButton().vm.$emit('click', { stopPropagation });
+ await nextTick();
+
+ expect(wrapper.text()).toBe(mockMasked);
+ expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
+ });
+ });
+
+ describe('When the copy to clipboard button is clicked', () => {
+ it('shows a copied message', () => {
+ findCopyButton().vm.$emit('success');
+
+ expect(showToast).toHaveBeenCalledTimes(1);
+ expect(showToast).toHaveBeenCalledWith('Registration token copied!');
+ });
+
+ it('does not fail when toast is not defined', () => {
+ createComponent({ withGlToast: false });
+ findCopyButton().vm.$emit('success');
+
+ // This block also tests for unhandled errors
+ expect(showToast).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_contacted_state_badge_spec.js b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js
new file mode 100644
index 00000000000..57a27f39826
--- /dev/null
+++ b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js
@@ -0,0 +1,86 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerContactedStateBadge from '~/runner/components/runner_contacted_state_badge.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_NOT_CONNECTED } from '~/runner/constants';
+
+describe('RunnerTypeBadge', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
+
+ const createComponent = ({ runner = {} } = {}) => {
+ wrapper = shallowMount(RunnerContactedStateBadge, {
+ propsData: {
+ runner: {
+ contactedAt: '2021-01-01T00:00:00Z',
+ status: STATUS_ONLINE,
+ ...runner,
+ },
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ jest.useFakeTimers('modern');
+ });
+
+ afterEach(() => {
+ jest.useFakeTimers('legacy');
+
+ wrapper.destroy();
+ });
+
+ it('renders online state', () => {
+ jest.setSystemTime(new Date('2021-01-01T00:01:00Z'));
+
+ createComponent();
+
+ expect(wrapper.text()).toBe('online');
+ expect(findBadge().props('variant')).toBe('success');
+ expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
+ });
+
+ it('renders offline state', () => {
+ jest.setSystemTime(new Date('2021-01-02T00:00:00Z'));
+
+ createComponent({
+ runner: {
+ status: STATUS_OFFLINE,
+ },
+ });
+
+ expect(wrapper.text()).toBe('offline');
+ expect(findBadge().props('variant')).toBe('muted');
+ expect(getTooltip().value).toBe(
+ 'No recent contact from this runner; last contact was 1 day ago',
+ );
+ });
+
+ it('renders not connected state', () => {
+ createComponent({
+ runner: {
+ contactedAt: null,
+ status: STATUS_NOT_CONNECTED,
+ },
+ });
+
+ expect(wrapper.text()).toBe('not connected');
+ expect(findBadge().props('variant')).toBe('muted');
+ expect(getTooltip().value).toMatch('This runner has never connected');
+ });
+
+ it('does not fail when data is missing', () => {
+ createComponent({
+ runner: {
+ status: null,
+ },
+ });
+
+ expect(wrapper.text()).toBe('');
+ });
+});
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
index 46948af1f28..9ea0955f2a1 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -5,13 +5,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_
import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
-import { typeTokenConfig } from '~/runner/components/search_tokens/type_token_config';
-import {
- PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
- PARAM_KEY_TAG,
- STATUS_ACTIVE,
-} from '~/runner/constants';
+import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, INSTANCE_TYPE } from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -31,6 +25,11 @@ describe('RunnerList', () => {
];
const mockActiveRunnersCount = 2;
+ const expectToHaveLastEmittedInput = (value) => {
+ const inputs = wrapper.emitted('input');
+ expect(inputs[inputs.length - 1][0]).toEqual(value);
+ };
+
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(RunnerFilteredSearchBar, {
@@ -38,6 +37,7 @@ describe('RunnerList', () => {
namespace: 'runners',
tokens: [],
value: {
+ runnerType: null,
filters: [],
sort: mockDefaultSort,
},
@@ -86,7 +86,7 @@ describe('RunnerList', () => {
it('sets tokens to the filtered search', () => {
createComponent({
props: {
- tokens: [statusTokenConfig, typeTokenConfig, tagTokenConfig],
+ tokens: [statusTokenConfig, tagTokenConfig],
},
});
@@ -97,11 +97,6 @@ describe('RunnerList', () => {
options: expect.any(Array),
}),
expect.objectContaining({
- type: PARAM_KEY_RUNNER_TYPE,
- token: BaseToken,
- options: expect.any(Array),
- }),
- expect.objectContaining({
type: PARAM_KEY_TAG,
token: TagToken,
}),
@@ -123,6 +118,7 @@ describe('RunnerList', () => {
createComponent({
props: {
value: {
+ runnerType: INSTANCE_TYPE,
sort: mockOtherSort,
filters: mockFilters,
},
@@ -142,30 +138,40 @@ describe('RunnerList', () => {
.text(),
).toEqual('Last contact');
});
+
+ it('when the user sets a filter, the "search" preserves the other filters', () => {
+ findGlFilteredSearch().vm.$emit('input', mockFilters);
+ findGlFilteredSearch().vm.$emit('submit');
+
+ expectToHaveLastEmittedInput({
+ runnerType: INSTANCE_TYPE,
+ filters: mockFilters,
+ sort: mockOtherSort,
+ pagination: { page: 1 },
+ });
+ });
});
it('when the user sets a filter, the "search" is emitted with filters', () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
- expect(wrapper.emitted('input')[0]).toEqual([
- {
- filters: mockFilters,
- sort: mockDefaultSort,
- pagination: { page: 1 },
- },
- ]);
+ expectToHaveLastEmittedInput({
+ runnerType: null,
+ filters: mockFilters,
+ sort: mockDefaultSort,
+ pagination: { page: 1 },
+ });
});
it('when the user sets a sorting method, the "search" is emitted with the sort', () => {
findSortOptions().at(1).vm.$emit('click');
- expect(wrapper.emitted('input')[0]).toEqual([
- {
- filters: [],
- sort: mockOtherSort,
- pagination: { page: 1 },
- },
- ]);
+ expectToHaveLastEmittedInput({
+ runnerType: null,
+ filters: [],
+ sort: mockOtherSort,
+ pagination: { page: 1 },
+ });
});
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index e24dffea1eb..986e55a2132 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -1,6 +1,5 @@
import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import { cloneDeep } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
@@ -43,12 +42,10 @@ describe('RunnerList', () => {
const headerLabels = findHeaders().wrappers.map((w) => w.text());
expect(headerLabels).toEqual([
- 'Type/State',
- 'Runner',
+ 'Status',
+ 'Runner ID',
'Version',
'IP Address',
- 'Projects',
- 'Jobs',
'Tags',
'Last contact',
'', // actions has no label
@@ -65,7 +62,7 @@ describe('RunnerList', () => {
const { id, description, version, ipAddress, shortSha } = mockRunners[0];
// Badges
- expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused');
+ expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused');
// Runner summary
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
@@ -76,8 +73,6 @@ describe('RunnerList', () => {
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1');
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
@@ -88,54 +83,6 @@ describe('RunnerList', () => {
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
- describe('Table data formatting', () => {
- let mockRunnersCopy;
-
- beforeEach(() => {
- mockRunnersCopy = cloneDeep(mockRunners);
- });
-
- it('Formats null project counts', () => {
- mockRunnersCopy[0].projectCount = null;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('n/a');
- });
-
- it('Formats 0 project counts', () => {
- mockRunnersCopy[0].projectCount = 0;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('0');
- });
-
- it('Formats big project counts', () => {
- mockRunnersCopy[0].projectCount = 1000;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1,000');
- });
-
- it('Formats job counts', () => {
- mockRunnersCopy[0].jobCount = 1000;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000');
- });
-
- it('Formats big job counts with a plus symbol', () => {
- mockRunnersCopy[0].jobCount = 1001;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+');
- });
- });
-
it('Shows runner identifier', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
diff --git a/spec/frontend/runner/components/runner_manual_setup_help_spec.js b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
deleted file mode 100644
index effef0e7ebf..00000000000
--- a/spec/frontend/runner/components/runner_manual_setup_help_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import { GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
-
-const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockRunnerInstallHelpPage = 'https://docs.gitlab.com/runner/install/';
-
-describe('RunnerManualSetupHelp', () => {
- let wrapper;
- let originalGon;
-
- const findRunnerInstructions = () => wrapper.findComponent(RunnerInstructions);
- const findRunnerRegistrationTokenReset = () =>
- wrapper.findComponent(RunnerRegistrationTokenReset);
- const findClipboardButtons = () => wrapper.findAllComponents(ClipboardButton);
- const findRunnerHelpTitle = () => wrapper.findByTestId('runner-help-title');
- const findCoordinatorUrl = () => wrapper.findByTestId('coordinator-url');
- const findRegistrationToken = () => wrapper.findByTestId('registration-token');
- const findRunnerHelpLink = () => wrapper.findByTestId('runner-help-link');
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = extendedWrapper(
- shallowMount(RunnerManualSetupHelp, {
- provide: {
- runnerInstallHelpPage: mockRunnerInstallHelpPage,
- },
- propsData: {
- registrationToken: mockRegistrationToken,
- type: INSTANCE_TYPE,
- ...props,
- },
- stubs: {
- MaskedValue,
- GlSprintf,
- },
- }),
- );
- };
-
- beforeAll(() => {
- originalGon = global.gon;
- global.gon = { gitlab_url: TEST_HOST };
- });
-
- afterAll(() => {
- global.gon = originalGon;
- });
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Title contains the shared runner type', () => {
- createComponent({ props: { type: INSTANCE_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a shared runner manually');
- });
-
- it('Title contains the group runner type', () => {
- createComponent({ props: { type: GROUP_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a group runner manually');
- });
-
- it('Title contains the specific runner type', () => {
- createComponent({ props: { type: PROJECT_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText(
- 'Set up a specific runner manually',
- );
- });
-
- it('Runner Install Page link', () => {
- expect(findRunnerHelpLink().attributes('href')).toBe(mockRunnerInstallHelpPage);
- });
-
- it('Displays the coordinator URL token', () => {
- expect(findCoordinatorUrl().text()).toBe(TEST_HOST);
- expect(findClipboardButtons().at(0).props('text')).toBe(TEST_HOST);
- });
-
- it('Displays the runner instructions', () => {
- expect(findRunnerInstructions().exists()).toBe(true);
- });
-
- it('Displays the registration token', async () => {
- findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
-
- await nextTick();
-
- expect(findRegistrationToken().text()).toBe(mockRegistrationToken);
- expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken);
- });
-
- it('Displays the runner registration token reset button', () => {
- expect(findRunnerRegistrationTokenReset().exists()).toBe(true);
- });
-
- it('Replaces the runner reset button', async () => {
- const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN';
-
- findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
- findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken);
-
- await nextTick();
-
- expect(findRegistrationToken().text()).toBe(mockNewRegistrationToken);
- expect(findClipboardButtons().at(1).props('text')).toBe(mockNewRegistrationToken);
- });
-});
diff --git a/spec/frontend/runner/components/runner_state_paused_badge_spec.js b/spec/frontend/runner/components/runner_paused_badge_spec.js
index 8df56d6e3f3..18cfcfae864 100644
--- a/spec/frontend/runner/components/runner_state_paused_badge_spec.js
+++ b/spec/frontend/runner/components/runner_paused_badge_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerStatePausedBadge from '~/runner/components/runner_state_paused_badge.vue';
+import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerTypeBadge', () => {
diff --git a/spec/frontend/runner/components/runner_state_locked_badge_spec.js b/spec/frontend/runner/components/runner_state_locked_badge_spec.js
deleted file mode 100644
index e92b671f5a1..00000000000
--- a/spec/frontend/runner/components/runner_state_locked_badge_spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RunnerStateLockedBadge from '~/runner/components/runner_state_locked_badge.vue';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-
-describe('RunnerTypeBadge', () => {
- let wrapper;
-
- const findBadge = () => wrapper.findComponent(GlBadge);
- const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(RunnerStateLockedBadge, {
- propsData: {
- ...props,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders locked state', () => {
- expect(wrapper.text()).toBe('locked');
- expect(findBadge().props('variant')).toBe('warning');
- });
-
- it('renders tooltip', () => {
- expect(getTooltip().value).toBeDefined();
- });
-
- it('passes arbitrary attributes to the badge', () => {
- createComponent({ props: { size: 'sm' } });
-
- expect(findBadge().props('size')).toBe('sm');
- });
-});
diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js
index dda318f8153..bd05d4b2cfe 100644
--- a/spec/frontend/runner/components/runner_tag_spec.js
+++ b/spec/frontend/runner/components/runner_tag_spec.js
@@ -1,18 +1,35 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+const mockTag = 'tag1';
describe('RunnerTag', () => {
let wrapper;
const findBadge = () => wrapper.findComponent(GlBadge);
+ const getTooltipValue = () => getBinding(findBadge().element, 'gl-tooltip').value;
+
+ const setDimensions = ({ scrollWidth, offsetWidth }) => {
+ jest.spyOn(findBadge().element, 'scrollWidth', 'get').mockReturnValue(scrollWidth);
+ jest.spyOn(findBadge().element, 'offsetWidth', 'get').mockReturnValue(offsetWidth);
+
+ // Mock trigger resize
+ getBinding(findBadge().element, 'gl-resize-observer').value();
+ };
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTag, {
propsData: {
- tag: 'tag1',
+ tag: mockTag,
...props,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ GlResizeObserver: createMockDirective(),
+ },
});
};
@@ -25,21 +42,36 @@ describe('RunnerTag', () => {
});
it('Displays tag text', () => {
- expect(wrapper.text()).toBe('tag1');
+ expect(wrapper.text()).toBe(mockTag);
});
it('Displays tags with correct style', () => {
expect(findBadge().props()).toMatchObject({
- size: 'md',
- variant: 'info',
+ size: 'sm',
+ variant: 'neutral',
});
});
- it('Displays tags with small size', () => {
+ it('Displays tags with md size', () => {
createComponent({
- props: { size: 'sm' },
+ props: { size: 'md' },
});
- expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('size')).toBe('md');
});
+
+ it.each`
+ case | scrollWidth | offsetWidth | expectedTooltip
+ ${'overflowing'} | ${110} | ${100} | ${mockTag}
+ ${'not overflowing'} | ${90} | ${100} | ${''}
+ ${'almost overflowing'} | ${100} | ${100} | ${''}
+ `(
+ 'Sets "$expectedTooltip" as tooltip when $case',
+ async ({ scrollWidth, offsetWidth, expectedTooltip }) => {
+ setDimensions({ scrollWidth, offsetWidth });
+ await nextTick();
+
+ expect(getTooltipValue()).toBe(expectedTooltip);
+ },
+ );
});
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js
index b6487ade0d6..da89a659432 100644
--- a/spec/frontend/runner/components/runner_tags_spec.js
+++ b/spec/frontend/runner/components/runner_tags_spec.js
@@ -33,16 +33,16 @@ describe('RunnerTags', () => {
});
it('Displays tags with correct style', () => {
- expect(findBadge().props('size')).toBe('md');
- expect(findBadge().props('variant')).toBe('info');
+ expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('variant')).toBe('neutral');
});
- it('Displays tags with small size', () => {
+ it('Displays tags with md size', () => {
createComponent({
- props: { size: 'sm' },
+ props: { size: 'md' },
});
- expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('size')).toBe('md');
});
it('Is empty when there are no tags', () => {
diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js
index e54e499743b..4023c75c9a8 100644
--- a/spec/frontend/runner/components/runner_type_alert_spec.js
+++ b/spec/frontend/runner/components/runner_type_alert_spec.js
@@ -23,11 +23,11 @@ describe('RunnerTypeAlert', () => {
});
describe.each`
- type | exampleText | anchor | variant
- ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'} | ${'success'}
- ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'} | ${'success'}
- ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'} | ${'info'}
- `('When it is an $type level runner', ({ type, exampleText, anchor, variant }) => {
+ type | exampleText | anchor
+ ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'}
+ ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'}
+ ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'}
+ `('When it is an $type level runner', ({ type, exampleText, anchor }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
@@ -36,8 +36,8 @@ describe('RunnerTypeAlert', () => {
expect(wrapper.text()).toMatch(exampleText);
});
- it(`Shows a ${variant} variant`, () => {
- expect(findAlert().props('variant')).toBe(variant);
+ it(`Shows an "info" variant`, () => {
+ expect(findAlert().props('variant')).toBe('info');
});
it(`Links to anchor "${anchor}"`, () => {
diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js
index fb344e65389..7bb0a2e6e2f 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/runner/components/runner_type_badge_spec.js
@@ -26,18 +26,18 @@ describe('RunnerTypeBadge', () => {
});
describe.each`
- type | text | variant
- ${INSTANCE_TYPE} | ${'shared'} | ${'success'}
- ${GROUP_TYPE} | ${'group'} | ${'success'}
- ${PROJECT_TYPE} | ${'specific'} | ${'info'}
- `('displays $type runner', ({ type, text, variant }) => {
+ type | text
+ ${INSTANCE_TYPE} | ${'shared'}
+ ${GROUP_TYPE} | ${'group'}
+ ${PROJECT_TYPE} | ${'specific'}
+ `('displays $type runner', ({ type, text }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
- it(`as "${text}" with a ${variant} variant`, () => {
+ it(`as "${text}" with an "info" variant`, () => {
expect(findBadge().text()).toBe(text);
- expect(findBadge().props('variant')).toBe(variant);
+ expect(findBadge().props('variant')).toBe('info');
});
it('with a tooltip', () => {
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js
new file mode 100644
index 00000000000..4871d9c470a
--- /dev/null
+++ b/spec/frontend/runner/components/runner_type_tabs_spec.js
@@ -0,0 +1,109 @@
+import { GlTab } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+
+const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
+
+describe('RunnerTypeTabs', () => {
+ let wrapper;
+
+ const findTabs = () => wrapper.findAll(GlTab);
+ const findActiveTab = () =>
+ findTabs()
+ .filter((tab) => tab.attributes('active') === 'true')
+ .at(0);
+
+ const createComponent = ({ props, ...options } = {}) => {
+ wrapper = shallowMount(RunnerTypeTabs, {
+ propsData: {
+ value: mockSearch,
+ ...props,
+ },
+ stubs: {
+ GlTab,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Renders options to filter runners', () => {
+ expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
+ 'All',
+ 'Instance',
+ 'Group',
+ 'Project',
+ ]);
+ });
+
+ it('"All" is selected by default', () => {
+ expect(findActiveTab().text()).toBe('All');
+ });
+
+ it('Another tab can be preselected by the user', () => {
+ createComponent({
+ props: {
+ value: {
+ ...mockSearch,
+ runnerType: INSTANCE_TYPE,
+ },
+ },
+ });
+
+ expect(findActiveTab().text()).toBe('Instance');
+ });
+
+ describe('When the user selects a tab', () => {
+ const emittedValue = () => wrapper.emitted('input')[0][0];
+
+ beforeEach(() => {
+ findTabs().at(2).vm.$emit('click');
+ });
+
+ it(`Runner type is emitted`, () => {
+ expect(emittedValue()).toEqual({
+ ...mockSearch,
+ runnerType: GROUP_TYPE,
+ });
+ });
+
+ it('Runner type is selected', async () => {
+ const newValue = emittedValue();
+ await wrapper.setProps({ value: newValue });
+
+ expect(findActiveTab().text()).toBe('Group');
+ });
+ });
+
+ describe('When using a custom slot', () => {
+ const mockContent = 'content';
+
+ beforeEach(() => {
+ createComponent({
+ scopedSlots: {
+ title: `
+ <span>
+ {{props.tab.title}} ${mockContent}
+ </span>`,
+ },
+ });
+ });
+
+ it('Renders tabs with additional information', () => {
+ expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
+ `All ${mockContent}`,
+ `Instance ${mockContent}`,
+ `Group ${mockContent}`,
+ `Project ${mockContent}`,
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 5f3aabd4bc3..39bca743c80 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
@@ -11,7 +12,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import {
@@ -19,8 +20,8 @@ import {
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
+ GROUP_TYPE,
PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
@@ -48,7 +49,7 @@ describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
- const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
@@ -82,13 +83,13 @@ describe('GroupRunnersApp', () => {
});
it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
});
it('shows the runners list', () => {
- expect(findRunnerList().props('runners')).toEqual(
- groupRunnersData.data.group.runners.edges.map(({ node }) => node),
- );
+ const runners = findRunnerList().props('runners');
+ expect(runners).toEqual(groupRunnersData.data.group.runners.edges.map(({ node }) => node));
});
it('runner item links to the runner group page', async () => {
@@ -117,16 +118,15 @@ describe('GroupRunnersApp', () => {
it('sets tokens in the filtered search', () => {
createComponent({ mountFn: mount });
- expect(findFilteredSearch().props('tokens')).toEqual([
+ const tokens = findFilteredSearch().props('tokens');
+
+ expect(tokens).toHaveLength(1);
+ expect(tokens[0]).toEqual(
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
- expect.objectContaining({
- type: PARAM_KEY_RUNNER_TYPE,
- options: expect.any(Array),
- }),
- ]);
+ );
});
describe('shows the active runner count', () => {
@@ -161,10 +161,8 @@ describe('GroupRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
- filters: [
- { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
- { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
- ],
+ runnerType: INSTANCE_TYPE,
+ filters: [{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
@@ -182,11 +180,14 @@ describe('GroupRunnersApp', () => {
});
describe('when a filter is selected by the user', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
+ runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
+
+ await nextTick();
});
it('updates the browser url', () => {
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index 3a0c3abe7bd..0fc7917663e 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -1,5 +1,6 @@
import { RUNNER_PAGE_SIZE } from '~/runner/constants';
import {
+ searchValidator,
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
@@ -10,13 +11,14 @@ describe('search_params.js', () => {
{
name: 'a default query',
urlQuery: '',
- search: { filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
+ search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
},
{
name: 'a single status',
urlQuery: '?status[]=ACTIVE',
search: {
+ runnerType: null,
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
@@ -27,6 +29,7 @@ describe('search_params.js', () => {
name: 'a single term text search',
urlQuery: '?search=something',
search: {
+ runnerType: null,
filters: [
{
type: 'filtered-search-term',
@@ -42,6 +45,7 @@ describe('search_params.js', () => {
name: 'a two terms text search',
urlQuery: '?search=something+else',
search: {
+ runnerType: null,
filters: [
{
type: 'filtered-search-term',
@@ -61,7 +65,8 @@ describe('search_params.js', () => {
name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: {
- filters: [{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }],
+ runnerType: 'INSTANCE_TYPE',
+ filters: [],
pagination: { page: 1 },
sort: 'CREATED_DESC',
},
@@ -71,6 +76,7 @@ describe('search_params.js', () => {
name: 'multiple runner status',
urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
search: {
+ runnerType: null,
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'status', value: { data: 'PAUSED', operator: '=' } },
@@ -84,10 +90,8 @@ describe('search_params.js', () => {
name: 'multiple status, a single instance type and a non default sort',
urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
search: {
- filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
- { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
- ],
+ runnerType: 'INSTANCE_TYPE',
+ filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_ASC',
},
@@ -102,6 +106,7 @@ describe('search_params.js', () => {
name: 'a tag',
urlQuery: '?tag[]=tag-1',
search: {
+ runnerType: null,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
@@ -116,6 +121,7 @@ describe('search_params.js', () => {
name: 'two tags',
urlQuery: '?tag[]=tag-1&tag[]=tag-2',
search: {
+ runnerType: null,
filters: [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
@@ -132,13 +138,19 @@ describe('search_params.js', () => {
{
name: 'the next page',
urlQuery: '?page=2&after=AFTER_CURSOR',
- search: { filters: [], pagination: { page: 2, after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC' },
+ search: {
+ runnerType: null,
+ filters: [],
+ pagination: { page: 2, after: 'AFTER_CURSOR' },
+ sort: 'CREATED_DESC',
+ },
graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
},
{
name: 'the previous page',
urlQuery: '?page=2&before=BEFORE_CURSOR',
search: {
+ runnerType: null,
filters: [],
pagination: { page: 2, before: 'BEFORE_CURSOR' },
sort: 'CREATED_DESC',
@@ -150,9 +162,9 @@ describe('search_params.js', () => {
urlQuery:
'?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
search: {
+ runnerType: 'INSTANCE_TYPE',
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
- { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
@@ -170,6 +182,14 @@ describe('search_params.js', () => {
},
];
+ describe('searchValidator', () => {
+ examples.forEach(({ name, search }) => {
+ it(`Validates ${name} as a search object`, () => {
+ expect(searchValidator(search)).toBe(true);
+ });
+ });
+ });
+
describe('fromUrlQueryToSearch', () => {
examples.forEach(({ name, urlQuery, search }) => {
it(`Converts ${name} to a search object`, () => {
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index b93527c1fe9..3bea0748c47 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -1,13 +1,13 @@
import { GlButton, GlLink } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('GlobalSearchSidebar', () => {
let wrapper;
@@ -20,28 +20,26 @@ describe('GlobalSearchSidebar', () => {
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
- query: MOCK_QUERY,
+ urlQuery: MOCK_QUERY,
...initialState,
},
actions: actionSpies,
});
wrapper = shallowMount(GlobalSearchSidebar, {
- localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
const findSidebarForm = () => wrapper.find('form');
- const findStatusFilter = () => wrapper.find(StatusFilter);
- const findConfidentialityFilter = () => wrapper.find(ConfidentialityFilter);
- const findApplyButton = () => wrapper.find(GlButton);
- const findResetLinkButton = () => wrapper.find(GlLink);
+ const findStatusFilter = () => wrapper.findComponent(StatusFilter);
+ const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
+ const findApplyButton = () => wrapper.findComponent(GlButton);
+ const findResetLinkButton = () => wrapper.findComponent(GlLink);
describe('template', () => {
beforeEach(() => {
@@ -61,10 +59,32 @@ describe('GlobalSearchSidebar', () => {
});
});
+ describe('ApplyButton', () => {
+ describe('when sidebarDirty is false', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: false });
+ });
+
+ it('disables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when sidebarDirty is true', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: true });
+ });
+
+ it('enables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ });
+ });
+ });
+
describe('ResetLinkButton', () => {
describe('with no filter selected', () => {
beforeEach(() => {
- createComponent({ query: {} });
+ createComponent({ urlQuery: {} });
});
it('does not render', () => {
@@ -74,10 +94,20 @@ describe('GlobalSearchSidebar', () => {
describe('with filter selected', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ urlQuery: MOCK_QUERY });
+ });
+
+ it('does render', () => {
+ expect(findResetLinkButton().exists()).toBe(true);
+ });
+ });
+
+ describe('with filter selected and user updated query back to default', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY, query: {} });
});
- it('does render when a filter selected', () => {
+ it('does render', () => {
expect(findResetLinkButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index b50248bb295..5f8cee8160f 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -5,7 +5,11 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
-import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
+import {
+ GROUPS_LOCAL_STORAGE_KEY,
+ PROJECTS_LOCAL_STORAGE_KEY,
+ SIDEBAR_PARAMS,
+} from '~/search/store/constants';
import * as types from '~/search/store/mutation_types';
import createState from '~/search/store/state';
import * as storeUtils from '~/search/store/utils';
@@ -153,15 +157,24 @@ describe('Global Search Store Actions', () => {
});
});
- describe('setQuery', () => {
- const payload = { key: 'key1', value: 'value1' };
+ describe.each`
+ payload | isDirty | isDirtyMutation
+ ${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]}
+ ${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]}
+ ${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]}
+ ${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]}
+ ${{ key: 'non-sidebar', value: 'test' }} | ${false} | ${[]}
+ ${{ key: 'non-sidebar', value: 'test' }} | ${true} | ${[]}
+ `('setQuery', ({ payload, isDirty, isDirtyMutation }) => {
+ describe(`when filter param is ${payload.key} and utils.isSidebarDirty returns ${isDirty}`, () => {
+ const expectedMutations = [{ type: types.SET_QUERY, payload }].concat(isDirtyMutation);
- it('calls the SET_QUERY mutation', () => {
- return testAction({
- action: actions.setQuery,
- payload,
- state,
- expectedMutations: [{ type: types.SET_QUERY, payload }],
+ beforeEach(() => {
+ storeUtils.isSidebarDirty = jest.fn().mockReturnValue(isDirty);
+ });
+
+ it(`should dispatch the correct mutations`, () => {
+ return testAction({ action: actions.setQuery, payload, state, expectedMutations });
});
});
});
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index a60718a972d..25f9b692955 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -72,6 +72,16 @@ describe('Global Search Store Mutations', () => {
});
});
+ describe('SET_SIDEBAR_DIRTY', () => {
+ const value = true;
+
+ it('sets sidebarDirty to the value', () => {
+ mutations[types.SET_SIDEBAR_DIRTY](state, value);
+
+ expect(state.sidebarDirty).toBe(value);
+ });
+ });
+
describe('LOAD_FREQUENT_ITEMS', () => {
it('sets frequentItems[key] to data', () => {
const payload = { key: 'test-key', data: [1, 2, 3] };
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index bcdad9f89dd..20d764190b1 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -1,6 +1,11 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { MAX_FREQUENCY } from '~/search/store/constants';
-import { loadDataFromLS, setFrequentItemToLS, mergeById } from '~/search/store/utils';
+import { MAX_FREQUENCY, SIDEBAR_PARAMS } from '~/search/store/constants';
+import {
+ loadDataFromLS,
+ setFrequentItemToLS,
+ mergeById,
+ isSidebarDirty,
+} from '~/search/store/utils';
import {
MOCK_LS_KEY,
MOCK_GROUPS,
@@ -216,4 +221,24 @@ describe('Global Search Store Utils', () => {
});
});
});
+
+ describe.each`
+ description | currentQuery | urlQuery | isDirty
+ ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${false}
+ ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${true}
+ ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${false}
+ ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${true}
+ `('isSidebarDirty', ({ description, currentQuery, urlQuery, isDirty }) => {
+ describe(`with ${description} sidebar query data`, () => {
+ let res;
+
+ beforeEach(() => {
+ res = isSidebarDirty(currentQuery, urlQuery);
+ });
+
+ it(`returns ${isDirty}`, () => {
+ expect(res).toStrictEqual(isDirty);
+ });
+ });
+ });
});
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index f27f45f2b26..d4ee9e6e43d 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,5 +1,6 @@
import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children';
@@ -70,6 +71,7 @@ describe('App component', () => {
const findTabs = () => wrapper.findAllComponents(GlTab);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
+ const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert');
const findLink = ({ href, text, container = wrapper }) => {
const selector = `a[href="${href}"]`;
const link = container.find(selector);
@@ -132,12 +134,12 @@ describe('App component', () => {
it('renders main-heading with correct text', () => {
const mainHeading = findMainHeading();
- expect(mainHeading).toExist();
+ expect(mainHeading.exists()).toBe(true);
expect(mainHeading.text()).toContain('Security Configuration');
});
it('renders GlTab Component ', () => {
- expect(findTab()).toExist();
+ expect(findTab().exists()).toBe(true);
});
it('renders right amount of tabs with correct title ', () => {
@@ -173,6 +175,43 @@ describe('App component', () => {
});
});
+ describe('Manage via MR Error Alert', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ });
+ });
+
+ describe('on initial load', () => {
+ it('should not show Manage via MR Error Alert', () => {
+ expect(findManageViaMRErrorAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('when error occurs', () => {
+ it('should show Alert with error Message', async () => {
+ expect(findManageViaMRErrorAlert().exists()).toBe(false);
+ findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+
+ await nextTick();
+ expect(findManageViaMRErrorAlert().exists()).toBe(true);
+ expect(findManageViaMRErrorAlert().text()).toEqual('There was a manage via MR error');
+ });
+
+ it('should hide Alert when it is dismissed', async () => {
+ findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+
+ await nextTick();
+ expect(findManageViaMRErrorAlert().exists()).toBe(true);
+
+ findManageViaMRErrorAlert().vm.$emit('dismiss');
+ await nextTick();
+ expect(findManageViaMRErrorAlert().exists()).toBe(false);
+ });
+ });
+ });
+
describe('Auto DevOps hint alert', () => {
describe('given the right props', () => {
beforeEach(() => {
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index fdb1d2f86e3..0eca2c27075 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -80,7 +80,11 @@ describe('FeatureCard component', () => {
describe('basic structure', () => {
beforeEach(() => {
- feature = makeFeature();
+ feature = makeFeature({
+ type: 'sast',
+ available: true,
+ canEnableByMergeRequest: true,
+ });
createComponent({ feature });
});
@@ -97,6 +101,11 @@ describe('FeatureCard component', () => {
expect(links.exists()).toBe(true);
expect(links).toHaveLength(1);
});
+
+ it('should catch and emit manage-via-mr-error', () => {
+ findManageViaMr().vm.$emit('error', 'There was a manage via MR error');
+ expect(wrapper.emitted('error')).toEqual([['There was a manage via MR error']]);
+ });
});
describe('status', () => {
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index 7e81df1d7d2..c72c23a3a60 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -10,7 +10,7 @@ const DEFAULT_RENDER_COUNT = 5;
describe('UncollapsedAssigneeList component', () => {
let wrapper;
- function createComponent(props = {}) {
+ function createComponent(props = {}, glFeatures = {}) {
const propsData = {
users: [],
rootPath: TEST_HOST,
@@ -19,6 +19,7 @@ describe('UncollapsedAssigneeList component', () => {
wrapper = mount(UncollapsedAssigneeList, {
propsData,
+ provide: { glFeatures },
});
}
@@ -99,4 +100,22 @@ describe('UncollapsedAssigneeList component', () => {
});
});
});
+
+ describe('merge requests', () => {
+ it.each`
+ numberOfUsers
+ ${1}
+ ${5}
+ `('displays as a vertical list for $numberOfUsers of users', ({ numberOfUsers }) => {
+ createComponent(
+ {
+ users: UsersMockHelper.createNumberRandomUsers(numberOfUsers),
+ issuableType: 'merge_request',
+ },
+ { mrAttentionRequests: true },
+ );
+
+ expect(wrapper.findAll('[data-testid="username"]').length).toBe(numberOfUsers);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/attention_required_toggle_spec.js b/spec/frontend/sidebar/components/attention_required_toggle_spec.js
new file mode 100644
index 00000000000..8555068cdd8
--- /dev/null
+++ b/spec/frontend/sidebar/components/attention_required_toggle_spec.js
@@ -0,0 +1,84 @@
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = mount(AttentionRequestedToggle, { propsData });
+}
+
+const findToggle = () => wrapper.findComponent(GlButton);
+
+describe('Attention require toggle', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders button', () => {
+ factory({ type: 'reviewer', user: { attention_requested: false } });
+
+ expect(findToggle().exists()).toBe(true);
+ });
+
+ it.each`
+ attentionRequested | icon
+ ${true} | ${'star'}
+ ${false} | ${'star-o'}
+ `(
+ 'renders $icon icon when attention_requested is $attentionRequested',
+ ({ attentionRequested, icon }) => {
+ factory({ type: 'reviewer', user: { attention_requested: attentionRequested } });
+
+ expect(findToggle().props('icon')).toBe(icon);
+ },
+ );
+
+ it.each`
+ attentionRequested | variant
+ ${true} | ${'warning'}
+ ${false} | ${'default'}
+ `(
+ 'renders button with variant $variant when attention_requested is $attentionRequested',
+ ({ attentionRequested, variant }) => {
+ factory({ type: 'reviewer', user: { attention_requested: attentionRequested } });
+
+ expect(findToggle().props('variant')).toBe(variant);
+ },
+ );
+
+ it('emits toggle-attention-requested on click', async () => {
+ factory({ type: 'reviewer', user: { attention_requested: true } });
+
+ await findToggle().trigger('click');
+
+ expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual([
+ {
+ user: { attention_requested: true },
+ callback: expect.anything(),
+ },
+ ]);
+ });
+
+ it('sets loading on click', async () => {
+ factory({ type: 'reviewer', user: { attention_requested: true } });
+
+ await findToggle().trigger('click');
+
+ expect(findToggle().props('loading')).toBe(true);
+ });
+
+ it.each`
+ type | attentionRequested | tooltip
+ ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequested}
+ ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedReviewer}
+ ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedAssignee}
+ `(
+ 'sets tooltip as $tooltip when attention_requested is $attentionRequested and type is $type',
+ ({ type, attentionRequested, tooltip }) => {
+ factory({ type, user: { attention_requested: attentionRequested } });
+
+ expect(findToggle().attributes('aria-label')).toBe(tooltip);
+ },
+ );
+});
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 6b80224083a..13887f28d22 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
import userDataMock from '../../user_data_mock';
@@ -9,7 +10,7 @@ describe('UncollapsedReviewerList component', () => {
const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
- function createComponent(props = {}) {
+ function createComponent(props = {}, glFeatures = {}) {
const propsData = {
users: [],
rootPath: TEST_HOST,
@@ -18,6 +19,9 @@ describe('UncollapsedReviewerList component', () => {
wrapper = shallowMount(UncollapsedReviewerList, {
propsData,
+ provide: {
+ glFeatures,
+ },
});
}
@@ -110,4 +114,18 @@ describe('UncollapsedReviewerList component', () => {
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
});
});
+
+ it('hides re-request review button when attentionRequired feature flag is enabled', () => {
+ createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
+
+ expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0);
+ });
+
+ it('emits toggle-attention-requested', () => {
+ createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
+
+ wrapper.find(AttentionRequestedToggle).vm.$emit('toggle-attention-requested', 'data');
+
+ expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual(['data']);
+ });
});
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 66218626e6b..64d143615a0 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -50,7 +50,7 @@ describe('Issuable Time Tracking Report', () => {
it('should render loading spinner', () => {
mountComponent();
- expect(findLoadingIcon()).toExist();
+ expect(findLoadingIcon().exists()).toBe(true);
});
it('should render error message on reject', async () => {
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index cb84c142d55..3d7baaff10a 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -4,8 +4,11 @@ import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
+import toast from '~/vue_shared/plugins/global_toast';
import Mock from './mock_data';
+jest.mock('~/vue_shared/plugins/global_toast');
+
describe('Sidebar mediator', () => {
const { mediator: mediatorMockData } = Mock;
let mock;
@@ -115,4 +118,56 @@ describe('Sidebar mediator', () => {
urlSpy.mockRestore();
});
});
+
+ describe('toggleAttentionRequested', () => {
+ let attentionRequiredService;
+
+ beforeEach(() => {
+ attentionRequiredService = jest
+ .spyOn(mediator.service, 'toggleAttentionRequested')
+ .mockResolvedValue();
+ });
+
+ it('calls attentionRequired service method', async () => {
+ mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
+
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ });
+
+ expect(attentionRequiredService).toHaveBeenCalledWith(1);
+ });
+
+ it.each`
+ type | method
+ ${'reviewer'} | ${'findReviewer'}
+ `('finds $type', ({ type, method }) => {
+ const methodSpy = jest.spyOn(mediator.store, method);
+
+ mediator.toggleAttentionRequested(type, { user: { id: 1 }, callback: jest.fn() });
+
+ expect(methodSpy).toHaveBeenCalledWith({ id: 1 });
+ });
+
+ it.each`
+ attentionRequested | toastMessage
+ ${true} | ${'Removed attention request from @root'}
+ ${false} | ${'Requested attention from @root'}
+ `(
+ 'it creates toast $toastMessage when attention_requested is $attentionRequested',
+ async ({ attentionRequested, toastMessage }) => {
+ mediator.store.reviewers = [
+ { id: 1, attention_requested: attentionRequested, username: 'root' },
+ ];
+
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ });
+
+ expect(toast).toHaveBeenCalledWith(toastMessage);
+ },
+ );
+ });
});
diff --git a/spec/frontend/task_list_spec.js b/spec/frontend/task_list_spec.js
index 2d7a735bd11..bf470e7e126 100644
--- a/spec/frontend/task_list_spec.js
+++ b/spec/frontend/task_list_spec.js
@@ -125,6 +125,7 @@ describe('TaskList', () => {
const response = { data: { lock_version: 3 } };
jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {});
jest.spyOn(taskList, 'disableTaskListItems').mockImplementation(() => {});
+ jest.spyOn(taskList, 'onUpdate').mockImplementation(() => {});
jest.spyOn(taskList, 'onSuccess').mockImplementation(() => {});
jest.spyOn(axios, 'patch').mockReturnValue(Promise.resolve(response));
@@ -151,8 +152,11 @@ describe('TaskList', () => {
},
};
- taskList
- .update(event)
+ const update = taskList.update(event);
+
+ expect(taskList.onUpdate).toHaveBeenCalled();
+
+ update
.then(() => {
expect(taskList.disableTaskListItems).toHaveBeenCalledWith(event);
expect(axios.patch).toHaveBeenCalledWith(endpoint, patchData);
@@ -168,12 +172,17 @@ describe('TaskList', () => {
it('should handle request error and enable task list items', (done) => {
const response = { data: { error: 1 } };
jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {});
+ jest.spyOn(taskList, 'onUpdate').mockImplementation(() => {});
jest.spyOn(taskList, 'onError').mockImplementation(() => {});
jest.spyOn(axios, 'patch').mockReturnValue(Promise.reject({ response })); // eslint-disable-line prefer-promise-reject-errors
const event = { detail: {} };
- taskList
- .update(event)
+
+ const update = taskList.update(event);
+
+ expect(taskList.onUpdate).toHaveBeenCalled();
+
+ update
.then(() => {
expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event);
expect(taskList.onError).toHaveBeenCalledWith(response.data);
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
new file mode 100644
index 00000000000..ee78b35843a
--- /dev/null
+++ b/spec/frontend/terms/components/app_spec.js
@@ -0,0 +1,171 @@
+import $ from 'jquery';
+import { merge } from 'lodash';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { nextTick } from 'vue';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import TermsApp from '~/terms/components/app.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+jest.mock('~/lib/utils/common_utils');
+
+describe('TermsApp', () => {
+ let wrapper;
+ let renderGFMSpy;
+
+ const defaultProvide = {
+ terms: 'foo bar',
+ paths: {
+ accept: '/-/users/terms/1/accept',
+ decline: '/-/users/terms/1/decline',
+ root: '/',
+ },
+ permissions: {
+ canAccept: true,
+ canDecline: true,
+ },
+ };
+
+ const createComponent = (provide = {}) => {
+ wrapper = mountExtended(TermsApp, {
+ provide: merge({}, defaultProvide, provide),
+ });
+ };
+
+ beforeEach(() => {
+ renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
+ isLoggedIn.mockReturnValue(true);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findFormWithAction = (path) => wrapper.find(`form[action="${path}"]`);
+ const findButton = (path) => findFormWithAction(path).find('button[type="submit"]');
+ const findScrollableViewport = () => wrapper.findByTestId('scrollable-viewport');
+
+ const expectFormWithSubmitButton = (buttonText, path) => {
+ const form = findFormWithAction(path);
+ const submitButton = findButton(path);
+
+ expect(form.exists()).toBe(true);
+ expect(submitButton.exists()).toBe(true);
+ expect(submitButton.text()).toBe(buttonText);
+ expect(
+ form
+ .find('input[type="hidden"][name="authenticity_token"][value="mock-csrf-token"]')
+ .exists(),
+ ).toBe(true);
+ };
+
+ it('renders terms of service as markdown', () => {
+ createComponent();
+
+ expect(wrapper.findByText(defaultProvide.terms).exists()).toBe(true);
+ expect(renderGFMSpy).toHaveBeenCalled();
+ });
+
+ describe('accept button', () => {
+ it('is disabled until user scrolls to the bottom of the terms', async () => {
+ createComponent();
+
+ expect(findButton(defaultProvide.paths.accept).attributes('disabled')).toBe('disabled');
+
+ wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+
+ await nextTick();
+
+ expect(findButton(defaultProvide.paths.accept).attributes('disabled')).toBeUndefined();
+ });
+
+ describe('when user has permissions to accept', () => {
+ it('renders form and button to accept terms', () => {
+ createComponent();
+
+ expectFormWithSubmitButton(TermsApp.i18n.accept, defaultProvide.paths.accept);
+ });
+ });
+
+ describe('when user does not have permissions to accept', () => {
+ it('renders continue button', () => {
+ createComponent({ permissions: { canAccept: false } });
+
+ expect(wrapper.findByText(TermsApp.i18n.continue).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('decline button', () => {
+ describe('when user has permissions to decline', () => {
+ it('renders form and button to decline terms', () => {
+ createComponent();
+
+ expectFormWithSubmitButton(TermsApp.i18n.decline, defaultProvide.paths.decline);
+ });
+ });
+
+ describe('when user does not have permissions to decline', () => {
+ it('does not render decline button', () => {
+ createComponent({ permissions: { canDecline: false } });
+
+ expect(wrapper.findByText(TermsApp.i18n.decline).exists()).toBe(false);
+ });
+ });
+ });
+
+ it('sets height of scrollable viewport', () => {
+ jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 800);
+ jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
+
+ createComponent();
+
+ expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 200px);');
+ });
+
+ describe('when flash is closed', () => {
+ let flashEl;
+
+ beforeEach(() => {
+ flashEl = document.createElement('div');
+ flashEl.classList.add(`flash-${FLASH_TYPES.ALERT}`);
+ document.body.appendChild(flashEl);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('recalculates height of scrollable viewport', () => {
+ jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 800);
+ jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
+
+ createComponent();
+
+ expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 200px);');
+
+ jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 700);
+ jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
+
+ flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
+
+ expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 100px);');
+ });
+ });
+
+ describe('when user is signed out', () => {
+ beforeEach(() => {
+ isLoggedIn.mockReturnValue(false);
+ });
+
+ it('does not show any buttons', () => {
+ createComponent();
+
+ expect(wrapper.findByText(TermsApp.i18n.accept).exists()).toBe(false);
+ expect(wrapper.findByText(TermsApp.i18n.decline).exists()).toBe(false);
+ expect(wrapper.findByText(TermsApp.i18n.continue).exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 2c8e0fff848..40f68c6385f 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -47,10 +47,12 @@ Object.assign(global, {
setFixtures: setHTMLFixture,
});
+const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
+
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
- // Don't override existing Jest matcher
- if (matcherName === 'toHaveLength') {
+ // Exclude these jQuery matchers
+ if (JQUERY_MATCHERS_TO_EXCLUDE.includes(matcherName)) {
return;
}
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js
index c9dea4394f9..c2606346292 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js
@@ -1,14 +1,20 @@
import { shallowMount } from '@vue/test-utils';
import { toNounSeriesText } from '~/lib/utils/grammar';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
-import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
+import {
+ APPROVED_BY_OTHERS,
+ APPROVED_BY_YOU,
+ APPROVED_BY_YOU_AND_OTHERS,
+} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+const exampleUserId = 1;
const testApprovers = () => Array.from({ length: 5 }, (_, i) => i).map((id) => ({ id }));
const testRulesLeft = () => ['Lorem', 'Ipsum', 'dolar & sit'];
const TEST_APPROVALS_LEFT = 3;
describe('MRWidget approvals summary', () => {
+ const originalUserId = gon.current_user_id;
let wrapper;
const createComponent = (props = {}) => {
@@ -28,6 +34,7 @@ describe('MRWidget approvals summary', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ gon.current_user_id = originalUserId;
});
describe('when approved', () => {
@@ -38,7 +45,7 @@ describe('MRWidget approvals summary', () => {
});
it('shows approved message', () => {
- expect(wrapper.text()).toContain(APPROVED_MESSAGE);
+ expect(wrapper.text()).toContain(APPROVED_BY_OTHERS);
});
it('renders avatar list for approvers', () => {
@@ -51,6 +58,48 @@ describe('MRWidget approvals summary', () => {
}),
);
});
+
+ describe('by the current user', () => {
+ beforeEach(() => {
+ gon.current_user_id = exampleUserId;
+ createComponent({
+ approvers: [{ id: exampleUserId }],
+ approved: true,
+ });
+ });
+
+ it('shows "Approved by you" message', () => {
+ expect(wrapper.text()).toContain(APPROVED_BY_YOU);
+ });
+ });
+
+ describe('by the current user and others', () => {
+ beforeEach(() => {
+ gon.current_user_id = exampleUserId;
+ createComponent({
+ approvers: [{ id: exampleUserId }, { id: exampleUserId + 1 }],
+ approved: true,
+ });
+ });
+
+ it('shows "Approved by you and others" message', () => {
+ expect(wrapper.text()).toContain(APPROVED_BY_YOU_AND_OTHERS);
+ });
+ });
+
+ describe('by other users than the current user', () => {
+ beforeEach(() => {
+ gon.current_user_id = exampleUserId;
+ createComponent({
+ approvers: [{ id: exampleUserId + 1 }],
+ approved: true,
+ });
+ });
+
+ it('shows "Approved by others" message', () => {
+ expect(wrapper.text()).toContain(APPROVED_BY_OTHERS);
+ });
+ });
});
describe('when not approved', () => {
diff --git a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js b/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
index d5d779d7a34..a13db2f4d72 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
+++ b/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
@@ -24,6 +24,18 @@ describe('MR widget extension actions', () => {
expect(wrapper.findAllComponents(GlButton)).toHaveLength(1);
});
+ it('calls action click handler', async () => {
+ const onClick = jest.fn();
+
+ factory({
+ tertiaryButtons: [{ text: 'hello world', onClick }],
+ });
+
+ await wrapper.findComponent(GlButton).vm.$emit('click');
+
+ expect(onClick).toHaveBeenCalled();
+ });
+
it('renders tertiary actions in dropdown', () => {
factory({
tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
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 ecaca16a2cd..6347e3c3be3 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
@@ -1,5 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
@@ -39,6 +41,8 @@ describe('MRWidgetPipeline', () => {
const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const mockArtifactsRequest = () => new MockAdapter(axios).onGet().reply(200, []);
+
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(PipelineComponent, {
@@ -71,6 +75,8 @@ describe('MRWidgetPipeline', () => {
describe('with a pipeline', () => {
beforeEach(() => {
+ mockArtifactsRequest();
+
createWrapper(
{
pipelineCoverageDelta: mockData.pipelineCoverageDelta,
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
index b5afc1ab21a..8e710b6d65f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
@@ -7,9 +7,7 @@ import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.v
import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import {
SP_TRACK_LABEL,
- SP_LINK_TRACK_EVENT,
SP_SHOW_TRACK_EVENT,
- SP_LINK_TRACK_VALUE,
SP_SHOW_TRACK_VALUE,
SP_HELP_URL,
} from '~/vue_merge_request_widget/constants';
@@ -52,15 +50,8 @@ describe('MRWidgetSuggestPipeline', () => {
mockAxios.restore();
});
- it('renders add pipeline file link', () => {
- const link = wrapper.find(GlLink);
-
- expect(link.exists()).toBe(true);
- expect(link.attributes().href).toBe(suggestProps.pipelinePath);
- });
-
it('renders the expected text', () => {
- const messageText = /\s*No pipeline\s*Add the .gitlab-ci.yml file\s*to create one./;
+ const messageText = /Looks like there's no pipeline here./;
expect(wrapper.text()).toMatch(messageText);
});
@@ -109,18 +100,6 @@ describe('MRWidgetSuggestPipeline', () => {
});
});
- it('send an event when add pipeline link is clicked', () => {
- mockTrackingOnWrapper();
- const link = wrapper.find('[data-testid="add-pipeline-link"]');
- triggerEvent(link.element);
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', SP_LINK_TRACK_EVENT, {
- label: SP_TRACK_LABEL,
- property: suggestProps.humanAccess,
- value: SP_LINK_TRACK_VALUE.toString(),
- });
- });
-
it('send an event when ok button is clicked', () => {
mockTrackingOnWrapper();
const okBtn = findOkBtn();
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
index 5981d2d7849..56a0218b374 100644
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -50,7 +50,7 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<span
class="gl-mr-3"
>
- The source branch will not be deleted
+ Does not delete the source branch
</span>
<gl-button-stub
@@ -122,7 +122,7 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<span
class="gl-mr-3"
>
- The source branch will not be deleted
+ Does not delete the source branch
</span>
<gl-button-stub
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
index a6c36764c41..f9936f22ea3 100644
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
@@ -9,7 +9,7 @@ exports[`New ready to merge state component renders permission text if canMerge
/>
<p
- class="media-body gl-m-0! gl-font-weight-bold"
+ class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
>
Ready to merge by members who can write to the target branch.
@@ -27,7 +27,7 @@ exports[`New ready to merge state component renders permission text if canMerge
/>
<p
- class="media-body gl-m-0! gl-font-weight-bold"
+ class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
>
Ready to merge!
diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
index 8214cedc4a1..f965fc32dc1 100644
--- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
@@ -3,6 +3,7 @@ import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit
const testCommitMessage = 'Test commit message';
const testLabel = 'Test label';
+const testTextMuted = 'Test text muted';
const testInputId = 'test-input-id';
describe('Commits edit component', () => {
@@ -63,7 +64,7 @@ describe('Commits edit component', () => {
beforeEach(() => {
createComponent({
header: `<div class="test-header">${testCommitMessage}</div>`,
- checkbox: `<label class="test-checkbox">${testLabel}</label >`,
+ 'text-muted': `<p class="test-text-muted">${testTextMuted}</p>`,
});
});
@@ -74,11 +75,11 @@ describe('Commits edit component', () => {
expect(headerSlotElement.text()).toBe(testCommitMessage);
});
- it('renders checkbox slot correctly', () => {
- const checkboxSlotElement = wrapper.find('.test-checkbox');
+ it('renders text-muted slot correctly', () => {
+ const textMutedElement = wrapper.find('.test-text-muted');
- expect(checkboxSlotElement.exists()).toBe(true);
- expect(checkboxSlotElement.text()).toBe(testLabel);
+ expect(textMutedElement.exists()).toBe(true);
+ expect(textMutedElement.text()).toBe(testTextMuted);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 4c1534574f5..d0a6af9970e 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -270,8 +270,8 @@ describe('MRWidgetAutoMergeEnabled', () => {
const normalizedText = wrapper.text().replace(/\s+/g, ' ');
- expect(normalizedText).toContain('The source branch will be deleted');
- expect(normalizedText).not.toContain('The source branch will not be deleted');
+ expect(normalizedText).toContain('Deletes the source branch');
+ expect(normalizedText).not.toContain('Does not delete the source branch');
});
it('should not show delete source branch button when user not able to delete source branch', () => {
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 2ff94a547f4..5858654e518 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,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
@@ -6,7 +6,7 @@ describe('Commits header component', () => {
let wrapper;
const createComponent = (props) => {
- wrapper = shallowMount(CommitsHeader, {
+ wrapper = mount(CommitsHeader, {
stubs: {
GlSprintf,
},
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 9c3a6d581e8..e0f1f091129 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -191,7 +191,7 @@ describe('MRWidgetMerged', () => {
});
it('shows button to copy commit SHA to clipboard', () => {
- expect(selectors.copyMergeShaButton).toExist();
+ expect(selectors.copyMergeShaButton).not.toBe(null);
expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(
vm.mr.mergeCommitSha,
);
@@ -201,14 +201,14 @@ describe('MRWidgetMerged', () => {
vm.mr.mergeCommitSha = null;
Vue.nextTick(() => {
- expect(selectors.copyMergeShaButton).not.toExist();
+ expect(selectors.copyMergeShaButton).toBe(null);
expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with');
done();
});
});
it('shows merge commit SHA link', () => {
- expect(selectors.mergeCommitShaLink).toExist();
+ expect(selectors.mergeCommitShaLink).not.toBe(null);
expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha);
expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
index b6c16958993..e6b2e9fa176 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
@@ -42,7 +42,7 @@ describe('MRWidgetMerging', () => {
.trim()
.replace(/\s\s+/g, ' ')
.replace(/[\r\n]+/g, ' '),
- ).toEqual('The changes will be merged into branch');
+ ).toEqual('Merges changes into branch');
expect(wrapper.find('a').attributes('href')).toBe('/branch-path');
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index f0fbb1d5851..016b6b2220b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -269,19 +269,6 @@ describe('ReadyToMerge', () => {
});
describe('methods', () => {
- describe('updateMergeCommitMessage', () => {
- it('should revert flag and change commitMessage', () => {
- createComponent();
-
- wrapper.vm.updateMergeCommitMessage(true);
-
- expect(wrapper.vm.commitMessage).toEqual(commitMessageWithDescription);
- wrapper.vm.updateMergeCommitMessage(false);
-
- expect(wrapper.vm.commitMessage).toEqual(commitMessage);
- });
- });
-
describe('handleMergeButtonClick', () => {
const returnPromise = (status) =>
new Promise((resolve) => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
index 8ead0002950..6abdbd11f5e 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -1,4 +1,4 @@
-import { GlFormCheckbox } from '@gitlab/ui';
+import { GlFormCheckbox, GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import { SQUASH_BEFORE_MERGE } from '~/vue_merge_request_widget/i18n';
@@ -77,7 +77,7 @@ describe('Squash before merge component', () => {
value: false,
});
- const aboutLink = wrapper.find('a');
+ const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.exists()).toBe(false);
});
@@ -88,7 +88,7 @@ describe('Squash before merge component', () => {
helpPath: 'test-path',
});
- const aboutLink = wrapper.find('a');
+ const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.exists()).toBe(true);
});
@@ -99,7 +99,7 @@ describe('Squash before merge component', () => {
helpPath: 'test-path',
});
- const aboutLink = wrapper.find('a');
+ const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.attributes('href')).toEqual('test-path');
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
index be15e4df66d..0fb0d5b0b68 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -46,7 +46,7 @@ describe('Wip', () => {
is_new_mr_data: true,
};
- describe('handleRemoveWIP', () => {
+ describe('handleRemoveDraft', () => {
it('should make a request to service and handle response', (done) => {
const vm = createComponent();
@@ -59,7 +59,7 @@ describe('Wip', () => {
}),
);
- vm.handleRemoveWIP();
+ vm.handleRemoveDraft();
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
@@ -84,7 +84,7 @@ describe('Wip', () => {
expect(el.innerText).toContain('This merge request is still a draft.');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge');
- expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain(
+ expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain(
'Mark as ready',
);
});
@@ -93,7 +93,7 @@ describe('Wip', () => {
vm.mr.removeWIPPath = '';
Vue.nextTick(() => {
- expect(el.querySelector('.js-remove-wip')).toEqual(null);
+ expect(el.querySelector('.js-remove-draft')).toEqual(null);
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 34a741cf8f2..f0c1da346a1 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -51,7 +51,7 @@ export default {
target_branch: 'main',
target_project_id: 19,
target_project_full_path: '/group2/project2',
- merge_request_add_ci_config_path: '/group2/project2/new/pipeline',
+ merge_request_add_ci_config_path: '/root/group2/project2/-/ci/editor',
is_dismissed_suggest_pipeline: false,
user_callouts_path: 'some/callout/path',
suggest_pipeline_feature_id: 'suggest_pipeline',
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 5aba6982886..550f156d095 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
+import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
@@ -6,6 +6,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
+import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
@@ -23,6 +24,8 @@ import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import mockData from './mock_data';
import testExtension from './test_extension';
+jest.mock('~/api.js');
+
jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
@@ -540,7 +543,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
const tooltip = wrapper.find('[data-testid="question-o-icon"]');
- expect(wrapper.text()).toContain('The source branch will be deleted');
+ expect(wrapper.text()).toContain('Deletes the source branch');
expect(tooltip.attributes('title')).toBe(
'A user with write access to the source branch selected this option',
);
@@ -556,7 +559,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
expect(wrapper.text()).toContain('The source branch has been deleted');
- expect(wrapper.text()).not.toContain('The source branch will be deleted');
+ expect(wrapper.text()).not.toContain('Deletes the source branch');
done();
});
@@ -904,6 +907,18 @@ describe('MrWidgetOptions', () => {
expect(wrapper.text()).toContain('Test extension summary count: 1');
});
+ it('triggers trackRedisHllUserEvent API call', async () => {
+ await waitForPromises();
+
+ wrapper
+ .find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
+ .trigger('click');
+
+ await Vue.nextTick();
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_expand_event');
+ });
+
it('renders full data', async () => {
await waitForPromises();
@@ -913,6 +928,10 @@ describe('MrWidgetOptions', () => {
await Vue.nextTick();
+ expect(
+ wrapper.find('[data-testid="widget-extension-top-level"]').find(GlDropdown).exists(),
+ ).toBe(false);
+
const collapsedSection = wrapper.find('[data-testid="widget-extension-collapsed-section"]');
expect(collapsedSection.exists()).toBe(true);
expect(collapsedSection.text()).toContain('Hello world');
@@ -928,6 +947,9 @@ describe('MrWidgetOptions', () => {
// Renders a link in the row
expect(collapsedSection.find(GlLink).exists()).toBe(true);
expect(collapsedSection.find(GlLink).text()).toBe('GitLab.com');
+
+ expect(collapsedSection.find(GlButton).exists()).toBe(true);
+ expect(collapsedSection.find(GlButton).text()).toBe('Full report');
});
});
});
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 631d4647b17..fc760f5c5be 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
@@ -15,7 +15,7 @@ describe('getStateKey', () => {
branchMissing: false,
commitsCount: 2,
hasConflicts: false,
- workInProgress: false,
+ draft: false,
};
const bound = getStateKey.bind(context);
@@ -49,9 +49,9 @@ describe('getStateKey', () => {
expect(bound()).toEqual('unresolvedDiscussions');
- context.workInProgress = true;
+ context.draft = true;
- expect(bound()).toEqual('workInProgress');
+ expect(bound()).toEqual('draft');
context.onlyAllowMergeIfPipelineSucceeds = true;
context.isPipelineFailed = true;
@@ -74,6 +74,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('nothingToMerge');
+ context.commitsCount = 1;
context.branchMissing = true;
expect(bound()).toEqual('missingBranch');
@@ -98,7 +99,7 @@ describe('getStateKey', () => {
branchMissing: false,
commitsCount: 2,
hasConflicts: false,
- workInProgress: false,
+ draft: false,
};
const bound = getStateKey.bind(context);
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 febcfcd4019..6eb68a1b00d 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
@@ -129,7 +129,7 @@ describe('MergeRequestStore', () => {
it('should set the add ci config path', () => {
store.setPaths({ ...mockData });
- expect(store.mergeRequestAddCiConfigPath).toBe('/group2/project2/new/pipeline');
+ expect(store.mergeRequestAddCiConfigPath).toBe('/root/group2/project2/-/ci/editor');
});
it('should set humanAccess=Maintainer when user has that role', () => {
diff --git a/spec/frontend/vue_mr_widget/test_extension.js b/spec/frontend/vue_mr_widget/test_extension.js
index a29a4d2fb46..65c1bd8473b 100644
--- a/spec/frontend/vue_mr_widget/test_extension.js
+++ b/spec/frontend/vue_mr_widget/test_extension.js
@@ -3,6 +3,7 @@ import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
export default {
name: 'WidgetTestExtension',
props: ['targetProjectFullPath'],
+ expandEvent: 'test_expand_event',
computed: {
summary({ count, targetProjectFullPath }) {
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
@@ -30,6 +31,7 @@ export default {
href: 'https://gitlab.com',
text: 'GitLab.com',
},
+ actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
},
]);
},
diff --git a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js b/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js
deleted file mode 100644
index b73f4d6a396..00000000000
--- a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import AlertDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
-
-describe('AlertDetails', () => {
- let wrapper;
-
- function mountComponent(hasManagedPrometheus = false) {
- wrapper = mount(AlertDeprecationWarning, {
- provide: {
- hasManagedPrometheus,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLink = () => wrapper.findComponent(GlLink);
-
- describe('Alert details', () => {
- describe('with no manual prometheus', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it('renders nothing', () => {
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- describe('with manual prometheus', () => {
- beforeEach(() => {
- mountComponent(true);
- });
-
- it('renders a deprecation notice', () => {
- expect(findAlert().text()).toContain('GitLab-managed Prometheus is deprecated');
- expect(findLink().attributes('href')).toContain(
- 'operations/metrics/alerts.html#managed-prometheus-instances',
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
new file mode 100644
index 00000000000..f75694bd504
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -0,0 +1,99 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import {
+ CONFIRM_DANGER_WARNING,
+ CONFIRM_DANGER_MODAL_BUTTON,
+ CONFIRM_DANGER_MODAL_ID,
+} from '~/vue_shared/components/confirm_danger/constants';
+import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('Confirm Danger Modal', () => {
+ const confirmDangerMessage = 'This is a dangerous activity';
+ const confirmButtonText = 'Confirm button text';
+ const phrase = 'You must construct additional pylons';
+ const modalId = CONFIRM_DANGER_MODAL_ID;
+
+ let wrapper;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findConfirmationPhrase = () => wrapper.findByTestId('confirm-danger-phrase');
+ const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-input');
+ const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning');
+ const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
+ const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+
+ const createComponent = ({ provide = {} } = {}) =>
+ shallowMountExtended(ConfirmDangerModal, {
+ propsData: {
+ modalId,
+ phrase,
+ },
+ provide,
+ stubs: { GlSprintf },
+ });
+
+ beforeEach(() => {
+ wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the default warning message', () => {
+ expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING);
+ });
+
+ it('renders any additional messages', () => {
+ expect(findAdditionalMessage().text()).toBe(confirmDangerMessage);
+ });
+
+ it('renders the confirm button', () => {
+ expect(findPrimaryAction().text).toBe(confirmButtonText);
+ expect(findPrimaryActionAttributes('variant')).toBe('danger');
+ });
+
+ it('renders the correct confirmation phrase', () => {
+ expect(findConfirmationPhrase().text()).toBe(
+ `Please type ${phrase} to proceed or close this modal to cancel.`,
+ );
+ });
+
+ describe('without injected data', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('does not render any additional messages', () => {
+ expect(findAdditionalMessage().exists()).toBe(false);
+ });
+
+ it('renders the default confirm button', () => {
+ expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON);
+ });
+ });
+
+ describe('with a valid confirmation phrase', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('enables the confirm button', async () => {
+ expect(findPrimaryActionAttributes('disabled')).toBe(true);
+
+ await findConfirmationInput().vm.$emit('input', phrase);
+
+ expect(findPrimaryActionAttributes('disabled')).toBe(false);
+ });
+
+ it('emits a `confirm` event when the button is clicked', async () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ await findConfirmationInput().vm.$emit('input', phrase);
+ await findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('confirm')).not.toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
new file mode 100644
index 00000000000..220f897c035
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
@@ -0,0 +1,61 @@
+import { GlButton } from '@gitlab/ui';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
+import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
+import { CONFIRM_DANGER_MODAL_ID } from '~/vue_shared/components/confirm_danger/constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('Confirm Danger Modal', () => {
+ let wrapper;
+
+ const phrase = 'En Taro Adun';
+ const buttonText = 'Click me!';
+ const modalId = CONFIRM_DANGER_MODAL_ID;
+
+ const findBtn = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(ConfirmDangerModal);
+ const findModalProps = () => findModal().props();
+
+ const createComponent = (props = {}) =>
+ shallowMountExtended(ConfirmDanger, {
+ propsData: {
+ buttonText,
+ phrase,
+ ...props,
+ },
+ });
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the button', () => {
+ expect(wrapper.html()).toContain(buttonText);
+ });
+
+ it('sets the modal properties', () => {
+ expect(findModalProps()).toMatchObject({
+ modalId,
+ phrase,
+ });
+ });
+
+ it('will disable the button if `disabled=true`', () => {
+ expect(findBtn().attributes('disabled')).toBeUndefined();
+
+ wrapper = createComponent({ disabled: true });
+
+ expect(findBtn().attributes('disabled')).toBe('true');
+ });
+
+ it('will emit `confirm` when the modal confirms', () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ findModal().vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')).not.toBeUndefined();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
index 16e7e4dd5cc..f28805471f8 100644
--- a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -16,6 +16,6 @@ describe('ContentViewer', () => {
propsData: { path, fileSize: 1024, type },
});
- expect(wrapper.find(selector).element).toExist();
+ expect(wrapper.find(selector).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 3ffb23dc7a0..1397fb0405e 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -42,7 +42,7 @@ describe('MarkdownViewer', () => {
it('renders an animation container while the markdown is loading', () => {
createComponent();
- expect(wrapper.find('.animation-container')).toExist();
+ expect(wrapper.find('.animation-container').exists()).toBe(true);
});
it('renders markdown preview preview renders and loads rendered markdown from server', () => {
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index 016fe1f131e..b3af5fd3feb 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -34,6 +34,7 @@ describe('DropdownWidget component', () => {
// invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
+ jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
beforeEach(() => {
@@ -67,10 +68,7 @@ describe('DropdownWidget component', () => {
});
it('emits set-option event when clicking on an option', async () => {
- wrapper
- .findAll('[data-testid="unselected-option"]')
- .at(1)
- .vm.$emit('click', new Event('click'));
+ wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
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 8e931aebfe0..64d15884333 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
@@ -25,6 +25,7 @@ import {
tokenValueMilestone,
tokenValueMembership,
tokenValueConfidential,
+ tokenValueEmpty,
} from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
@@ -43,6 +44,7 @@ const createComponent = ({
recentSearchesStorageKey = 'requirements',
tokens = mockAvailableTokens,
sortOptions,
+ initialFilterValue = [],
showCheckbox = false,
checkboxChecked = false,
searchInputPlaceholder = 'Filter requirements',
@@ -55,6 +57,7 @@ const createComponent = ({
recentSearchesStorageKey,
tokens,
sortOptions,
+ initialFilterValue,
showCheckbox,
checkboxChecked,
searchInputPlaceholder,
@@ -193,19 +196,27 @@ describe('FilteredSearchBarRoot', () => {
describe('watchers', () => {
describe('filterValue', () => {
- it('emits component event `onFilter` with empty array when `filterValue` is cleared by GlFilteredSearch', () => {
+ it('emits component event `onFilter` with empty array and false when filter was never selected', () => {
+ wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] });
wrapper.setData({
initialRender: false,
- filterValue: [
- {
- type: 'filtered-search-term',
- value: { data: '' },
- },
- ],
+ filterValue: [tokenValueEmpty],
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]);
+ });
+ });
+
+ it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => {
+ wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
+ wrapper.setData({
+ initialRender: false,
+ filterValue: [tokenValueEmpty],
});
return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('onFilter')[0]).toEqual([[]]);
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
});
});
});
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 ae02c554e13..238c5d16db5 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
@@ -9,6 +9,7 @@ import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_t
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_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';
+import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
export const mockAuthor1 = {
@@ -110,6 +111,18 @@ export const mockIterationToken = {
fetchIterations: () => Promise.resolve(),
};
+export const mockIterations = [
+ {
+ id: 1,
+ title: 'Iteration 1',
+ startDate: '2021-11-05',
+ dueDate: '2021-11-10',
+ iterationCadence: {
+ title: 'Cadence 1',
+ },
+ },
+];
+
export const mockLabelToken = {
type: 'label_name',
icon: 'labels',
@@ -132,6 +145,14 @@ export const mockMilestoneToken = {
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
+export const mockReleaseToken = {
+ type: 'release',
+ icon: 'rocket',
+ title: 'Release',
+ token: ReleaseToken,
+ fetchReleases: () => Promise.resolve(),
+};
+
export const mockEpicToken = {
type: 'epic_iid',
icon: 'clock',
@@ -282,6 +303,11 @@ export const tokenValuePlain = {
value: { data: 'foo' },
};
+export const tokenValueEmpty = {
+ type: 'filtered-search-term',
+ value: { data: '' },
+};
+
export const tokenValueEpic = {
type: 'epic_iid',
value: {
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 14fcffd3c50..b29c394e7ae 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
@@ -112,6 +112,35 @@ describe('AuthorToken', () => {
});
});
+ // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
+ describe('when there are null users presents', () => {
+ const mockAuthorsWithNullUser = mockAuthors.concat([null]);
+
+ beforeEach(() => {
+ jest
+ .spyOn(wrapper.vm.config, 'fetchAuthors')
+ .mockResolvedValue({ data: mockAuthorsWithNullUser });
+
+ getBaseToken().vm.$emit('fetch-suggestions', 'root');
+ });
+
+ describe('when res.data is present', () => {
+ it('filters the successful response when null values are present', () => {
+ return waitForPromises().then(() => {
+ expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ });
+ });
+ });
+
+ describe('when response is an array', () => {
+ it('filters the successful response when null values are present', () => {
+ return waitForPromises().then(() => {
+ expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ });
+ });
+ });
+ });
+
it('calls `createFlash` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
index af90ee93543..44bc16adb97 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
@@ -1,9 +1,13 @@
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchTokenSegment,
+ GlFilteredSearchSuggestion,
+} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
-import { mockIterationToken } from '../mock_data';
+import { mockIterationToken, mockIterations } from '../mock_data';
jest.mock('~/flash');
@@ -11,10 +15,16 @@ describe('IterationToken', () => {
const id = 123;
let wrapper;
- const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) =>
+ const createComponent = ({
+ config = mockIterationToken,
+ value = { data: '' },
+ active = false,
+ stubs = {},
+ provide = {},
+ } = {}) =>
mount(IterationToken, {
propsData: {
- active: false,
+ active,
config,
value,
},
@@ -22,13 +32,39 @@ describe('IterationToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ ...provide,
},
+ stubs,
});
afterEach(() => {
wrapper.destroy();
});
+ describe('when iteration cadence feature is available', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockIterationToken, initialIterations: mockIterations },
+ value: { data: 'i' },
+ stubs: { Portal: true },
+ provide: {
+ glFeatures: {
+ iterationCadences: true,
+ },
+ },
+ });
+
+ await wrapper.setData({ loading: false });
+ });
+
+ it('renders iteration start date and due date', () => {
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions.at(3).text()).toContain('Nov 5, 2021 - Nov 10, 2021');
+ });
+ });
+
it('renders iteration value', async () => {
wrapper = createComponent({ value: { data: id } });
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
new file mode 100644
index 00000000000..b804ff97b82
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -0,0 +1,78 @@
+import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
+import { mockReleaseToken } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('ReleaseToken', () => {
+ const id = 123;
+ let wrapper;
+
+ const createComponent = ({ config = mockReleaseToken, value = { data: '' } } = {}) =>
+ mount(ReleaseToken, {
+ propsData: {
+ active: false,
+ config,
+ value,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: () => 'custom-class',
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders release value', async () => {
+ wrapper = createComponent({ value: { data: id } });
+ await wrapper.vm.$nextTick();
+
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // `Release` `=` `v1`
+ expect(tokenSegments.at(2).text()).toBe(id.toString());
+ });
+
+ it('fetches initial values', () => {
+ const fetchReleasesSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ value: { data: id },
+ });
+
+ expect(fetchReleasesSpy).toHaveBeenCalledWith(id);
+ });
+
+ it('fetches releases on user input', () => {
+ const search = 'hello';
+ const fetchReleasesSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ });
+
+ wrapper.findComponent(GlFilteredSearchToken).vm.$emit('input', { data: search });
+
+ expect(fetchReleasesSpy).toHaveBeenCalledWith(search);
+ });
+
+ it('renders error message when request fails', async () => {
+ const fetchReleasesSpy = jest.fn().mockRejectedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching releases.',
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 42f4439df51..b76f475a6fb 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue';
@@ -18,6 +18,7 @@ describe('Header CI Component', () => {
},
time: '2017-05-08T14:57:39.781Z',
user: {
+ id: 1234,
web_url: 'path',
name: 'Foo',
username: 'foobar',
@@ -29,7 +30,7 @@ describe('Header CI Component', () => {
const findIconBadge = () => wrapper.findComponent(CiIconBadge);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
- const findUserLink = () => wrapper.findComponent(GlLink);
+ const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons');
const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text');
@@ -64,10 +65,6 @@ describe('Header CI Component', () => {
expect(findTimeAgo().exists()).toBe(true);
});
- it('should render user icon and name', () => {
- expect(findUserLink().text()).toContain(defaultProps.user.name);
- });
-
it('should render sidebar toggle button', () => {
expect(findSidebarToggleBtn().exists()).toBe(true);
});
@@ -77,6 +74,45 @@ describe('Header CI Component', () => {
});
});
+ describe('user avatar', () => {
+ beforeEach(() => {
+ createComponent({ itemName: 'Pipeline' });
+ });
+
+ it('contains the username', () => {
+ expect(findUserLink().text()).toContain(defaultProps.user.username);
+ });
+
+ it('has the correct data attributes', () => {
+ expect(findUserLink().attributes()).toMatchObject({
+ 'data-user-id': defaultProps.user.id.toString(),
+ 'data-username': defaultProps.user.username,
+ 'data-name': defaultProps.user.name,
+ });
+ });
+
+ describe('with data from GraphQL', () => {
+ const userId = 1;
+
+ beforeEach(() => {
+ createComponent({
+ itemName: 'Pipeline',
+ user: { ...defaultProps.user, id: `gid://gitlab/User/${1}` },
+ });
+ });
+
+ it('has the correct user id', () => {
+ expect(findUserLink().attributes('data-user-id')).toBe(userId.toString());
+ });
+ });
+
+ describe('with data from REST', () => {
+ it('has the correct user id', () => {
+ expect(findUserLink().attributes('data-user-id')).toBe(defaultProps.user.id.toString());
+ });
+ });
+ });
+
describe('with item id', () => {
beforeEach(() => {
createComponent({ itemName: 'Pipeline', itemId: '123' });
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 48dacc50923..65f79bab005 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,13 +1,27 @@
+import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
import initMRPopovers from '~/mr_popover/index';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/mr_popover/index', () => jest.fn());
describe('system note component', () => {
let vm;
let props;
+ let mock;
+
+ function createComponent(propsData = {}) {
+ const store = createStore();
+ store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
+
+ vm = mount(IssueSystemNote, {
+ store,
+ propsData,
+ });
+ }
beforeEach(() => {
props = {
@@ -27,28 +41,29 @@ describe('system note component', () => {
},
};
- const store = createStore();
- store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
-
- vm = mount(IssueSystemNote, {
- store,
- propsData: props,
- });
+ mock = new MockAdapter(axios);
});
afterEach(() => {
vm.destroy();
+ mock.restore();
});
it('should render a list item with correct id', () => {
+ createComponent(props);
+
expect(vm.attributes('id')).toEqual(`note_${props.note.id}`);
});
it('should render target class is note is target note', () => {
+ createComponent(props);
+
expect(vm.classes()).toContain('target');
});
it('should render svg icon', () => {
+ createComponent(props);
+
expect(vm.find('.timeline-icon svg').exists()).toBe(true);
});
@@ -56,10 +71,31 @@ describe('system note component', () => {
// we need to strip them because they break layout of commit lists in system notes:
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
it('removes wrapping paragraph from note HTML', () => {
+ createComponent(props);
+
expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>');
});
it('should initMRPopovers onMount', () => {
+ createComponent(props);
+
expect(initMRPopovers).toHaveBeenCalled();
});
+
+ it('renders outdated code lines', async () => {
+ mock
+ .onGet('/outdated_line_change_path')
+ .reply(200, [
+ { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
+ ]);
+
+ createComponent({
+ note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
+ });
+
+ await vm.find("[data-testid='outdated-lines-change-btn']").trigger('click');
+ await waitForPromises();
+
+ expect(vm.find("[data-testid='outdated-lines']").exists()).toBe(true);
+ });
});
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 1ed7844b395..7fdacbe83a2 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
@@ -1,6 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
+import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
@@ -13,8 +12,7 @@ describe('ProjectListItem component', () => {
let vm;
let options;
- // eslint-disable-next-line import/no-deprecated
- const project = getJSONFixture('static/projects.json')[0];
+ const project = JSON.parse(JSON.stringify(mockProjects))[0];
beforeEach(() => {
options = {
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index 1f97d3ff3fa..de5cee846a1 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -2,8 +2,7 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import { head } from 'lodash';
import Vue from 'vue';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
+import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
@@ -13,8 +12,7 @@ const localVue = createLocalVue();
describe('ProjectSelector component', () => {
let wrapper;
let vm;
- // eslint-disable-next-line import/no-deprecated
- const allProjects = getJSONFixture('static/projects.json');
+ const allProjects = mockProjects;
const searchResults = allProjects.slice(0, 5);
let selected = [];
selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index 75aa3bc7096..b62676b35be 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -1,5 +1,6 @@
import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import component from '~/vue_shared/components/registry/title_area.vue';
describe('title area', () => {
@@ -7,18 +8,18 @@ describe('title area', () => {
const DYNAMIC_SLOT = 'metadata-dynamic-slot';
- 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 findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]');
- const findDynamicSlot = () => wrapper.find(`[data-testid="${DYNAMIC_SLOT}`);
+ const findSubHeaderSlot = () => wrapper.findByTestId('sub-header');
+ const findRightActionsSlot = () => wrapper.findByTestId('right-actions');
+ const findMetadataSlot = (name) => wrapper.findByTestId(name);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findInfoMessages = () => wrapper.findAllByTestId('info-message');
+ const findDynamicSlot = () => wrapper.findByTestId(DYNAMIC_SLOT);
const findSlotOrderElements = () => wrapper.findAll('[slot-test]');
- const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
propsData,
stubs: { GlSprintf },
slots: {
@@ -29,6 +30,12 @@ describe('title area', () => {
});
};
+ const generateSlotMocks = (names) =>
+ names.reduce((acc, current) => {
+ acc[current] = `<div data-testid="${current}" />`;
+ return acc;
+ }, {});
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -40,6 +47,7 @@ describe('title area', () => {
expect(findTitle().text()).toBe('foo');
});
+
it('if slot is present uses slot', () => {
mountComponent({
slots: {
@@ -88,24 +96,21 @@ describe('title area', () => {
${['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;
- }, {});
+ const slots = generateSlotMocks(slotNames);
it('exist when the slot is present', async () => {
- mountComponent({ slots: slotMocks });
+ mountComponent({ slots });
- await wrapper.vm.$nextTick();
+ await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(true);
});
});
it('is/are hidden when metadata-loading is true', async () => {
- mountComponent({ slots: slotMocks, propsData: { title: 'foo', metadataLoading: true } });
+ mountComponent({ slots, propsData: { title: 'foo', metadataLoading: true } });
- await wrapper.vm.$nextTick();
+ await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(false);
});
@@ -113,14 +118,20 @@ describe('title area', () => {
});
describe('metadata skeleton loader', () => {
- it('is hidden when metadata loading is false', () => {
- mountComponent();
+ const slots = generateSlotMocks(['metadata-foo']);
+
+ it('is hidden when metadata loading is false', async () => {
+ mountComponent({ slots });
+
+ await nextTick();
expect(findSkeletonLoader().exists()).toBe(false);
});
- it('is shown when metadata loading is true', () => {
- mountComponent({ propsData: { metadataLoading: true } });
+ it('is shown when metadata loading is true', async () => {
+ mountComponent({ propsData: { metadataLoading: true }, slots });
+
+ await nextTick();
expect(findSkeletonLoader().exists()).toBe(true);
});
@@ -143,7 +154,7 @@ describe('title area', () => {
// updating the slots like we do on line 141 does not cause the updated lifecycle-hook to be triggered
wrapper.vm.$forceUpdate();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDynamicSlot().exists()).toBe(true);
});
@@ -163,7 +174,7 @@ describe('title area', () => {
// updating the slots like we do on line 159 does not cause the updated lifecycle-hook to be triggered
wrapper.vm.$forceUpdate();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findSlotOrderElements().at(0).attributes('data-testid')).toBe(DYNAMIC_SLOT);
expect(findSlotOrderElements().at(1).attributes('data-testid')).toBe('metadata-foo');
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 32ef2d27ba7..8536ffed573 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlModal, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
@@ -52,7 +52,7 @@ describe('RunnerInstructionsModal component', () => {
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
const findRegisterCommand = () => wrapper.findByTestId('register-command');
- const createComponent = () => {
+ const createComponent = ({ props, ...options } = {}) => {
const requestHandlers = [
[getRunnerPlatformsQuery, runnerPlatformsHandler],
[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
@@ -64,9 +64,12 @@ describe('RunnerInstructionsModal component', () => {
shallowMount(RunnerInstructionsModal, {
propsData: {
modalId: 'runner-instructions-modal',
+ registrationToken: 'MY_TOKEN',
+ ...props,
},
localVue,
apolloProvider: fakeApollo,
+ ...options,
}),
);
};
@@ -118,18 +121,30 @@ describe('RunnerInstructionsModal component', () => {
expect(instructions).toBe(installInstructions);
});
- it('register command is shown', () => {
+ it('register command is shown with a replaced token', () => {
const instructions = findRegisterCommand().text();
- expect(instructions).toBe(registerInstructions);
+ expect(instructions).toBe(
+ 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
+ });
+
+ describe('when a register token is not shown', () => {
+ beforeEach(async () => {
+ createComponent({ props: { registrationToken: undefined } });
+ await nextTick();
+ });
+
+ it('register command is shown without a defined registration token', () => {
+ const instructions = findRegisterCommand().text();
+
+ expect(instructions).toBe(registerInstructions);
+ });
});
});
describe('after a platform and architecture are selected', () => {
- const {
- installInstructions,
- registerInstructions,
- } = mockGraphqlInstructionsWindows.data.runnerSetup;
+ const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
beforeEach(async () => {
runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
@@ -157,7 +172,9 @@ describe('RunnerInstructionsModal component', () => {
it('register command is shown', () => {
const command = findRegisterCommand().text();
- expect(command).toBe(registerInstructions);
+ expect(command).toBe(
+ './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
});
});
@@ -217,4 +234,36 @@ describe('RunnerInstructionsModal component', () => {
expect(findRegisterCommand().exists()).toBe(false);
});
});
+
+ describe('GlModal API', () => {
+ const getGlModalStub = (methods) => {
+ return {
+ ...GlModal,
+ methods: {
+ ...GlModal.methods,
+ ...methods,
+ },
+ };
+ };
+
+ describe('show()', () => {
+ let mockShow;
+
+ beforeEach(() => {
+ mockShow = jest.fn();
+
+ createComponent({
+ stubs: {
+ GlModal: getGlModalStub({ show: mockShow }),
+ },
+ });
+ });
+
+ it('delegates show()', () => {
+ wrapper.vm.show();
+
+ expect(mockShow).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
index 165caea2751..a0f46f07d6a 100644
--- a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
+++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
@@ -50,6 +50,7 @@ exports[`Settings Block renders the correct markup 1`] = `
class="settings-content"
id="settings_content_3"
role="region"
+ style="display: none;"
tabindex="-1"
>
<div
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
index 528dfd89690..5e829653c13 100644
--- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -1,12 +1,12 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
describe('Settings Block', () => {
let wrapper;
const mountComponent = (propsData) => {
- wrapper = shallowMount(SettingsBlock, {
+ wrapper = shallowMountExtended(SettingsBlock, {
propsData,
slots: {
title: '<div data-testid="title-slot"></div>',
@@ -20,11 +20,13 @@ describe('Settings Block', () => {
wrapper.destroy();
});
- const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
- const findTitleSlot = () => wrapper.find('[data-testid="title-slot"]');
- const findDescriptionSlot = () => wrapper.find('[data-testid="description-slot"]');
+ const findDefaultSlot = () => wrapper.findByTestId('default-slot');
+ const findTitleSlot = () => wrapper.findByTestId('title-slot');
+ const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
const findExpandButton = () => wrapper.findComponent(GlButton);
- const findSectionTitleButton = () => wrapper.find('[data-testid="section-title-button"]');
+ const findSectionTitleButton = () => wrapper.findByTestId('section-title-button');
+ // we are using a non js class for this finder because this class determine the component structure
+ const findSettingsContent = () => wrapper.find('.settings-content');
const expectExpandedState = ({ expanded = true } = {}) => {
const settingsExpandButton = findExpandButton();
@@ -62,6 +64,26 @@ describe('Settings Block', () => {
expect(findDescriptionSlot().exists()).toBe(true);
});
+ it('content is hidden before first expansion', async () => {
+ // this is a regression test for the bug described here: https://gitlab.com/gitlab-org/gitlab/-/issues/331774
+ mountComponent();
+
+ // content is hidden
+ expect(findDefaultSlot().isVisible()).toBe(false);
+
+ // expand
+ await findSectionTitleButton().trigger('click');
+
+ // content is visible
+ expect(findDefaultSlot().isVisible()).toBe(true);
+
+ // collapse
+ await findSectionTitleButton().trigger('click');
+
+ // content is still visible (and we have a closing animation)
+ expect(findDefaultSlot().isVisible()).toBe(true);
+ });
+
describe('slide animation behaviour', () => {
it('is animated by default', () => {
mountComponent();
@@ -81,6 +103,20 @@ describe('Settings Block', () => {
expect(wrapper.classes('no-animate')).toBe(noAnimatedClass);
},
);
+
+ it('sets the animating class only during the animation', async () => {
+ mountComponent();
+
+ expect(wrapper.classes('animating')).toBe(false);
+
+ await findSectionTitleButton().trigger('click');
+
+ expect(wrapper.classes('animating')).toBe(true);
+
+ await findSettingsContent().trigger('animationend');
+
+ expect(wrapper.classes('animating')).toBe(false);
+ });
});
describe('expanded behaviour', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
index 240d6cb5a34..79e41ed0c9e 100644
--- a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -1,36 +1,68 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-describe('collapsedCalendarIcon', () => {
- let vm;
- beforeEach(() => {
- const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
- vm = mountComponent(CollapsedCalendarIcon, {
- containerClass: 'test-class',
- text: 'text',
- showIcon: false,
+import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+
+describe('CollapsedCalendarIcon', () => {
+ let wrapper;
+
+ const defaultProps = {
+ containerClass: 'test-class',
+ text: 'text',
+ tooltipText: 'tooltip text',
+ showIcon: false,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(CollapsedCalendarIcon, {
+ propsData: { ...defaultProps, ...props },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
});
- it('should add class to container', () => {
- expect(vm.$el.classList.contains('test-class')).toEqual(true);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
+
+ it('adds class to container', () => {
+ expect(wrapper.classes()).toContain(defaultProps.containerClass);
+ });
+
+ it('does not render calendar icon when showIcon is false', () => {
+ expect(findGlIcon().exists()).toBe(false);
+ });
+
+ it('renders calendar icon when showIcon is true', () => {
+ createComponent({
+ props: { showIcon: true },
+ });
+
+ expect(findGlIcon().exists()).toBe(true);
});
- it('should hide calendar icon if showIcon', () => {
- expect(vm.$el.querySelector('[data-testid="calendar-icon"]')).toBeNull();
+ it('renders text', () => {
+ expect(wrapper.text()).toBe(defaultProps.text);
});
- it('should render text', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
+ it('renders tooltipText as tooltip', () => {
+ expect(getTooltip().value).toBe(defaultProps.tooltipText);
});
- it('should emit click event when container is clicked', () => {
- const click = jest.fn();
- vm.$on('click', click);
+ it('emits click event when container is clicked', async () => {
+ wrapper.trigger('click');
- vm.$el.click();
+ await wrapper.vm.$nextTick();
- expect(click).toHaveBeenCalled();
+ expect(wrapper.emitted('click')[0]).toBeDefined();
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
index 230442ec547..e72b3bf45c4 100644
--- a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -1,86 +1,103 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
-
-describe('collapsedGroupedDatePicker', () => {
- let vm;
- beforeEach(() => {
- const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
- vm = mountComponent(CollapsedGroupedDatePicker, {
- showToggleSidebar: true,
+import { shallowMount } from '@vue/test-utils';
+
+import CollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
+import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+
+describe('CollapsedGroupedDatePicker', () => {
+ let wrapper;
+
+ const defaultProps = {
+ showToggleSidebar: true,
+ };
+
+ const minDate = new Date('07/17/2016');
+ const maxDate = new Date('07/17/2017');
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(CollapsedGroupedDatePicker, {
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
});
- describe('toggleCollapse events', () => {
- beforeEach((done) => {
- jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {});
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
+ const findCollapsedCalendarIcon = () => wrapper.findComponent(CollapsedCalendarIcon);
+ const findAllCollapsedCalendarIcons = () => wrapper.findAllComponents(CollapsedCalendarIcon);
+ describe('toggleCollapse events', () => {
it('should emit when collapsed-calendar-icon is clicked', () => {
- vm.$el.querySelector('.sidebar-collapsed-icon').click();
+ createComponent();
- expect(vm.toggleSidebar).toHaveBeenCalled();
+ findCollapsedCalendarIcon().trigger('click');
+
+ expect(wrapper.emitted('toggleCollapse')[0]).toBeDefined();
});
});
describe('minDate and maxDate', () => {
- beforeEach((done) => {
- vm.minDate = new Date('07/17/2016');
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
it('should render both collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(2);
- expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
- expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
+ createComponent({
+ props: {
+ minDate,
+ maxDate,
+ },
+ });
+
+ const icons = findAllCollapsedCalendarIcons();
+
+ expect(icons.length).toBe(2);
+ expect(icons.at(0).text()).toBe('Jul 17 2016');
+ expect(icons.at(1).text()).toBe('Jul 17 2017');
});
});
describe('minDate', () => {
- beforeEach((done) => {
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
-
it('should render minDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ createComponent({
+ props: {
+ minDate,
+ },
+ });
+
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('From Jul 17 2016');
});
});
describe('maxDate', () => {
- beforeEach((done) => {
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
it('should render maxDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
+ createComponent({
+ props: {
+ maxDate,
+ },
+ });
+ const icons = findAllCollapsedCalendarIcons();
+
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('Until Jul 17 2017');
});
});
describe('no dates', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('should render None', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('None');
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('None');
});
it('should have tooltip as `Start and due date`', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons[0].title).toBe('Start and due date');
+ expect(icons.at(0).props('tooltipText')).toBe('Start and due date');
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
index 3221e88192b..263d1e9d947 100644
--- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
@@ -1,3 +1,4 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import DatePicker from '~/vue_shared/components/pikaday.vue';
import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
@@ -5,14 +6,8 @@ import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
describe('SidebarDatePicker', () => {
let wrapper;
- const mountComponent = (propsData = {}, data = {}) => {
- if (wrapper) {
- throw new Error('tried to call mountComponent without d');
- }
+ const createComponent = (propsData = {}, data = {}) => {
wrapper = mount(SidebarDatePicker, {
- stubs: {
- DatePicker: true,
- },
propsData,
data: () => data,
});
@@ -20,87 +15,93 @@ describe('SidebarDatePicker', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
+ const findDatePicker = () => wrapper.findComponent(DatePicker);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEditButton = () => wrapper.find('.title .btn-blank');
+ const findRemoveButton = () => wrapper.find('.value-content .btn-blank');
+ const findSidebarToggle = () => wrapper.find('.title .gutter-toggle');
+ const findValueContent = () => wrapper.find('.value-content');
+
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
- mountComponent();
+ createComponent();
- wrapper.find('.issuable-sidebar-header .gutter-toggle').element.click();
+ wrapper.find('.issuable-sidebar-header .gutter-toggle').trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
it('should render collapsed-calendar-icon', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.sidebar-collapsed-icon').element).toBeDefined();
+ expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(true);
});
it('should render value when not editing', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.value-content').element).toBeDefined();
+ expect(findValueContent().exists()).toBe(true);
});
it('should render None if there is no selectedDate', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.value-content span').text().trim()).toEqual('None');
+ expect(findValueContent().text()).toBe('None');
});
it('should render date-picker when editing', () => {
- mountComponent({}, { editing: true });
+ createComponent({}, { editing: true });
- expect(wrapper.find(DatePicker).element).toBeDefined();
+ expect(findDatePicker().exists()).toBe(true);
});
it('should render label', () => {
const label = 'label';
- mountComponent({ label });
- expect(wrapper.find('.title').text().trim()).toEqual(label);
+ createComponent({ label });
+ expect(wrapper.find('.title').text()).toBe(label);
});
it('should render loading-icon when isLoading', () => {
- mountComponent({ isLoading: true });
- expect(wrapper.find('.gl-spinner').element).toBeDefined();
+ createComponent({ isLoading: true });
+ expect(findLoadingIcon().exists()).toBe(true);
});
describe('editable', () => {
beforeEach(() => {
- mountComponent({ editable: true });
+ createComponent({ editable: true });
});
it('should render edit button', () => {
- expect(wrapper.find('.title .btn-blank').text().trim()).toEqual('Edit');
+ expect(findEditButton().text()).toBe('Edit');
});
it('should enable editing when edit button is clicked', async () => {
- wrapper.find('.title .btn-blank').element.click();
+ findEditButton().trigger('click');
await wrapper.vm.$nextTick();
- expect(wrapper.vm.editing).toEqual(true);
+ expect(wrapper.vm.editing).toBe(true);
});
});
it('should render date if selectedDate', () => {
- mountComponent({ selectedDate: new Date('07/07/2017') });
+ createComponent({ selectedDate: new Date('07/07/2017') });
- expect(wrapper.find('.value-content strong').text().trim()).toEqual('Jul 7, 2017');
+ expect(wrapper.find('.value-content strong').text()).toBe('Jul 7, 2017');
});
describe('selectedDate and editable', () => {
beforeEach(() => {
- mountComponent({ selectedDate: new Date('07/07/2017'), editable: true });
+ createComponent({ selectedDate: new Date('07/07/2017'), editable: true });
});
it('should render remove button if selectedDate and editable', () => {
- expect(wrapper.find('.value-content .btn-blank').text().trim()).toEqual('remove');
+ expect(findRemoveButton().text()).toBe('remove');
});
it('should emit saveDate with null when remove button is clicked', () => {
- wrapper.find('.value-content .btn-blank').element.click();
+ findRemoveButton().trigger('click');
expect(wrapper.emitted('saveDate')).toEqual([[null]]);
});
@@ -108,15 +109,15 @@ describe('SidebarDatePicker', () => {
describe('showToggleSidebar', () => {
beforeEach(() => {
- mountComponent({ showToggleSidebar: true });
+ createComponent({ showToggleSidebar: true });
});
it('should render toggle-sidebar when showToggleSidebar', () => {
- expect(wrapper.find('.title .gutter-toggle').element).toBeDefined();
+ expect(findSidebarToggle().exists()).toBe(true);
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
- wrapper.find('.title .gutter-toggle').element.click();
+ findSidebarToggle().trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
index 8c1693e8dcc..a7f9391cb5f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -1,95 +1,74 @@
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
+import { mockCollapsedLabels as mockLabels, mockRegularLabel } from './mock_data';
-import { mockCollapsedLabels as mockLabels } from './mock_data';
-
-const createComponent = (labels = mockLabels) => {
- const Component = Vue.extend(dropdownValueCollapsedComponent);
+describe('DropdownValueCollapsedComponent', () => {
+ let wrapper;
- return mountComponent(Component, {
- labels,
- });
-};
+ const defaultProps = {
+ labels: [],
+ };
-describe('DropdownValueCollapsedComponent', () => {
- let vm;
+ const mockManyLabels = [...mockLabels, ...mockLabels, ...mockLabels];
- beforeEach(() => {
- vm = createComponent();
- });
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(DropdownValueCollapsedComponent, {
+ propsData: { ...defaultProps, ...props },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('computed', () => {
- describe('labelsList', () => {
- it('returns default text when `labels` prop is empty array', () => {
- const vmEmptyLabels = createComponent([]);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
- expect(vmEmptyLabels.labelsList).toBe('Labels');
- vmEmptyLabels.$destroy();
- });
-
- it('returns labels names separated by coma when `labels` prop has more than one item', () => {
- const labels = mockLabels.concat(mockLabels);
- const vmMoreLabels = createComponent(labels);
+ describe('template', () => {
+ it('renders tags icon element', () => {
+ createComponent();
- const expectedText = labels.map((label) => label.title).join(', ');
+ expect(findGlIcon().exists()).toBe(true);
+ });
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
- });
+ it('emits onValueClick event on click', async () => {
+ createComponent();
- it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => {
- const mockMoreLabels = Object.assign([], mockLabels);
- for (let i = 0; i < 6; i += 1) {
- mockMoreLabels.unshift(mockLabels[0]);
- }
+ wrapper.trigger('click');
- const vmMoreLabels = createComponent(mockMoreLabels);
+ await wrapper.vm.$nextTick();
- const expectedText = `${mockMoreLabels
- .slice(0, 5)
- .map((label) => label.title)
- .join(', ')}, and ${mockMoreLabels.length - 5} more`;
+ expect(wrapper.emitted('onValueClick')[0]).toBeDefined();
+ });
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
+ describe.each`
+ scenario | labels | expectedResult | expectedText
+ ${'`labels` is empty'} | ${[]} | ${'default text'} | ${'Labels'}
+ ${'`labels` has 1 item'} | ${[mockRegularLabel]} | ${'label name'} | ${'Foo Label'}
+ ${'`labels` has 2 items'} | ${mockLabels} | ${'comma separated label names'} | ${'Foo Label, Foo::Bar'}
+ ${'`labels` has more than 5 items'} | ${mockManyLabels} | ${'comma separated label names with "and more" phrase'} | ${'Foo Label, Foo::Bar, Foo Label, Foo::Bar, Foo Label, and 1 more'}
+ `('when $scenario', ({ labels, expectedResult, expectedText }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ labels,
+ },
+ });
});
- it('returns first label name when `labels` prop has only one item present', () => {
- const text = mockLabels.map((label) => label.title).join(', ');
-
- expect(vm.labelsList).toBe(text);
+ it('renders labels count', () => {
+ expect(wrapper.text()).toBe(`${labels.length}`);
});
- });
- });
-
- describe('methods', () => {
- describe('handleClick', () => {
- it('emits onValueClick event on component', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.handleClick();
- expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
+ it(`renders "${expectedResult}" as tooltip`, () => {
+ expect(getTooltip().value).toBe(expectedText);
});
});
});
-
- describe('template', () => {
- it('renders component container element with tooltip`', () => {
- expect(vm.$el.title).toBe(vm.labelsList);
- });
-
- it('renders tags icon element', () => {
- expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull();
- });
-
- it('renders labels count', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`);
- });
- });
});
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 d9b7cd5afa2..a60e6f52862 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
@@ -1,3 +1,4 @@
+import { cloneDeep } from 'lodash';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
@@ -153,47 +154,40 @@ describe('LabelsSelect Mutations', () => {
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
- let labels;
-
- beforeEach(() => {
- labels = [
- { id: 1, title: 'scoped' },
- { id: 2, title: 'scoped::one', set: false },
- { id: 3, title: 'scoped::test', set: true },
- { id: 4, title: '' },
- ];
- });
-
- it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
- const updatedLabelIds = [2];
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
-
- state.labels.forEach((label) => {
- if (updatedLabelIds.includes(label.id)) {
- expect(label.touched).toBe(true);
- expect(label.set).toBe(true);
- }
+ const labels = [
+ { id: 1, title: 'scoped' },
+ { id: 2, title: 'scoped::label::one', set: false },
+ { id: 3, title: 'scoped::label::two', set: false },
+ { id: 4, title: 'scoped::label::three', set: true },
+ { id: 5, title: 'scoped::one', set: false },
+ { id: 6, title: 'scoped::two', set: false },
+ { id: 7, title: 'scoped::three', set: true },
+ { id: 8, title: '' },
+ ];
+
+ it.each`
+ label | labelGroupIds
+ ${labels[0]} | ${[]}
+ ${labels[1]} | ${[labels[2], labels[3]]}
+ ${labels[2]} | ${[labels[1], labels[3]]}
+ ${labels[3]} | ${[labels[1], labels[2]]}
+ ${labels[4]} | ${[labels[5], labels[6]]}
+ ${labels[5]} | ${[labels[4], labels[6]]}
+ ${labels[6]} | ${[labels[4], labels[5]]}
+ ${labels[7]} | ${[]}
+ `('updates `touched` and `set` props for $label.title', ({ label, labelGroupIds }) => {
+ const state = { labels: cloneDeep(labels) };
+
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: label.id }] });
+
+ expect(state.labels[label.id - 1]).toMatchObject({
+ touched: true,
+ set: !labels[label.id - 1].set,
});
- });
- describe('when label is scoped', () => {
- it('unsets the currently selected scoped label and sets the current label', () => {
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, {
- labels: [{ id: 2, title: 'scoped::one' }],
- });
-
- expect(state.labels).toEqual([
- { id: 1, title: 'scoped' },
- { id: 2, title: 'scoped::one', set: true, touched: true },
- { id: 3, title: 'scoped::test', set: false },
- { id: 4, title: '' },
- ]);
+ labelGroupIds.forEach((l) => {
+ expect(state.labels[l.id - 1].touched).toBeFalsy();
+ expect(state.labels[l.id - 1].set).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 8931584e12c..bf873f9162b 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -5,8 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
-import { labelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
import {
@@ -50,11 +49,12 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({
mutationHandler = createLabelSuccessHandler,
- issuableType = IssuableType.Issue,
+ labelCreateType = 'project',
+ workspaceType = 'project',
} = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
- query: labelsQueries[issuableType].workspaceQuery,
+ query: workspaceLabelsQueries[workspaceType].query,
data: workspaceLabelsQueryResponse.data,
variables: {
fullPath: '',
@@ -66,8 +66,10 @@ describe('DropdownContentsCreateView', () => {
localVue,
apolloProvider: mockApollo,
propsData: {
- issuableType,
fullPath: '',
+ attrWorkspacePath: '',
+ labelCreateType,
+ workspaceType,
},
});
};
@@ -128,9 +130,11 @@ describe('DropdownContentsCreateView', () => {
it('emits a `hideCreateView` event on Cancel button click', () => {
createComponent();
- findCancelButton().vm.$emit('click');
+ const event = { stopPropagation: jest.fn() };
+ findCancelButton().vm.$emit('click', event);
expect(wrapper.emitted('hideCreateView')).toHaveLength(1);
+ expect(event.stopPropagation).toHaveBeenCalled();
});
describe('when label title and selected color are set', () => {
@@ -174,7 +178,7 @@ describe('DropdownContentsCreateView', () => {
});
it('calls a mutation with `groupPath` variable on the epic', () => {
- createComponent({ issuableType: IssuableType.Epic });
+ createComponent({ labelCreateType: 'group', workspaceType: 'group' });
fillLabelAttributes();
findCreateButton().vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index fac3331a2b8..2980409fdce 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -10,7 +10,6 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
@@ -43,6 +42,7 @@ describe('DropdownContentsLabelsView', () => {
initialState = mockConfig,
queryHandler = successfulQueryHandler,
injected = {},
+ searchKey = '',
} = {}) => {
const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]);
@@ -56,7 +56,9 @@ describe('DropdownContentsLabelsView', () => {
propsData: {
...initialState,
localSelectedLabels,
- issuableType: IssuableType.Issue,
+ searchKey,
+ labelCreateType: 'project',
+ workspaceType: 'project',
},
stubs: {
GlSearchBoxByType,
@@ -68,7 +70,6 @@ describe('DropdownContentsLabelsView', () => {
wrapper.destroy();
});
- const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
@@ -81,12 +82,6 @@ describe('DropdownContentsLabelsView', () => {
}
describe('when loading labels', () => {
- it('renders disabled search input field', async () => {
- createComponent();
- await makeObserverAppear();
- expect(findSearchInput().props('disabled')).toBe(true);
- });
-
it('renders loading icon', async () => {
createComponent();
await makeObserverAppear();
@@ -107,10 +102,6 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises();
});
- it('renders enabled search input field', async () => {
- expect(findSearchInput().props('disabled')).toBe(false);
- });
-
it('does not render loading icon', async () => {
expect(findLoadingIcon().exists()).toBe(false);
});
@@ -132,9 +123,9 @@ describe('DropdownContentsLabelsView', () => {
},
},
}),
+ searchKey: '123',
});
await makeObserverAppear();
- findSearchInput().vm.$emit('input', '123');
await waitForPromises();
await nextTick();
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
index 36704ac5ef3..8bcef347c96 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
@@ -4,6 +4,8 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
+import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
import { mockLabels } from './mock_data';
@@ -26,7 +28,7 @@ const GlDropdownStub = {
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
+ const createComponent = ({ props = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
labelsCreateTitle: 'test',
@@ -37,8 +39,10 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels',
variant: 'sidebar',
- issuableType: 'issue',
fullPath: 'test',
+ workspaceType: 'project',
+ labelCreateType: 'project',
+ attrWorkspacePath: 'path',
...props,
},
data() {
@@ -46,11 +50,6 @@ describe('DropdownContent', () => {
...data,
};
},
- provide: {
- allowLabelCreate: true,
- labelsManagePath: 'foo/bar',
- ...injected,
- },
stubs: {
GlDropdown: GlDropdownStub,
},
@@ -63,13 +62,10 @@ describe('DropdownContent', () => {
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
+ const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
+ const findDropdownFooter = () => wrapper.findComponent(DropdownFooter);
const findDropdown = () => wrapper.findComponent(GlDropdownStub);
- const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
- const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
- const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
- const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
-
it('calls dropdown `show` method on `isVisible` prop change', async () => {
createComponent();
await wrapper.setProps({
@@ -136,6 +132,16 @@ describe('DropdownContent', () => {
expect(findDropdownHeader().exists()).toBe(true);
});
+ it('sets searchKey for labels view on input event from header', async () => {
+ createComponent();
+
+ expect(wrapper.vm.searchKey).toEqual('');
+ findDropdownHeader().vm.$emit('input', '123');
+ await nextTick();
+
+ expect(findLabelsView().props('searchKey')).toEqual('123');
+ });
+
describe('Create view', () => {
beforeEach(() => {
createComponent({ data: { showDropdownContentsCreateView: true } });
@@ -149,16 +155,8 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(false);
});
- it('does not render create label button', () => {
- expect(findCreateLabelButton().exists()).toBe(false);
- });
-
- it('renders go back button', () => {
- expect(findGoBackButton().exists()).toBe(true);
- });
-
- it('changes the view to Labels view on back button click', async () => {
- findGoBackButton().vm.$emit('click', new MouseEvent('click'));
+ it('changes the view to Labels view on `toggleDropdownContentsCreateView` event', async () => {
+ findDropdownHeader().vm.$emit('toggleDropdownContentsCreateView');
await nextTick();
expect(findCreateView().exists()).toBe(false);
@@ -198,32 +196,5 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(true);
});
-
- it('does not render go back button', () => {
- expect(findGoBackButton().exists()).toBe(false);
- });
-
- it('does not render create label button if `allowLabelCreate` is false', () => {
- createComponent({ injected: { allowLabelCreate: false } });
-
- expect(findCreateLabelButton().exists()).toBe(false);
- });
-
- describe('when `allowLabelCreate` is true', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders create label button', () => {
- expect(findCreateLabelButton().exists()).toBe(true);
- });
-
- it('changes the view to Create on create label button click', async () => {
- findCreateLabelButton().trigger('click');
-
- await nextTick();
- expect(findLabelsView().exists()).toBe(false);
- });
- });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
new file mode 100644
index 00000000000..0508a059195
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+
+describe('DropdownFooter', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {}, injected = {} } = {}) => {
+ wrapper = shallowMount(DropdownFooter, {
+ propsData: {
+ footerCreateLabelTitle: 'create',
+ footerManageLabelTitle: 'manage',
+ ...props,
+ },
+ provide: {
+ allowLabelCreate: true,
+ labelsManagePath: 'foo/bar',
+ ...injected,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
+
+ describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render create label button if `allowLabelCreate` is false', () => {
+ createComponent({ injected: { allowLabelCreate: false } });
+
+ expect(findCreateLabelButton().exists()).toBe(false);
+ });
+
+ describe('when `allowLabelCreate` is true', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders create label button', () => {
+ expect(findCreateLabelButton().exists()).toBe(true);
+ });
+
+ it('emits `toggleDropdownContentsCreateView` event on create label button click', async () => {
+ findCreateLabelButton().trigger('click');
+
+ await nextTick();
+ expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
new file mode 100644
index 00000000000..592559ef305
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
@@ -0,0 +1,75 @@
+import { GlSearchBoxByType } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+
+describe('DropdownHeader', () => {
+ let wrapper;
+
+ const createComponent = ({
+ showDropdownContentsCreateView = false,
+ labelsFetchInProgress = false,
+ } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(DropdownHeader, {
+ propsData: {
+ showDropdownContentsCreateView,
+ labelsFetchInProgress,
+ labelsCreateTitle: 'Create label',
+ labelsListTitle: 'Select label',
+ searchKey: '',
+ },
+ stubs: {
+ GlSearchBoxByType,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findGoBackButton = () => wrapper.findByTestId('go-back-button');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Create view', () => {
+ beforeEach(() => {
+ createComponent({ showDropdownContentsCreateView: true });
+ });
+
+ it('renders go back button', () => {
+ expect(findGoBackButton().exists()).toBe(true);
+ });
+
+ it('does not render search input field', async () => {
+ expect(findSearchInput().exists()).toBe(false);
+ });
+ });
+
+ describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render go back button', () => {
+ expect(findGoBackButton().exists()).toBe(false);
+ });
+
+ it.each`
+ labelsFetchInProgress | disabled
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when labelsFetchInProgress is $labelsFetchInProgress, renders search input with disabled prop to $disabled',
+ ({ labelsFetchInProgress, disabled }) => {
+ createComponent({ labelsFetchInProgress });
+ expect(findSearchInput().props('disabled')).toBe(disabled);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index b5441d711a5..d4203528874 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -41,6 +41,8 @@ describe('LabelsSelectRoot', () => {
propsData: {
...config,
issuableType: IssuableType.Issue,
+ labelCreateType: 'project',
+ workspaceType: 'project',
},
stubs: {
SidebarEditableItem,
@@ -121,11 +123,11 @@ describe('LabelsSelectRoot', () => {
});
});
- it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => {
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
- createComponent();
+ createComponent({ config: { ...mockConfig, iid: undefined } });
findDropdownContents().vm.$emit('setLabels', [label]);
- expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]);
+ expect(wrapper.emitted('updateSelectedLabels')).toEqual([[{ labels: [label] }]]);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 23a457848d9..5c5bf5f2187 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -40,12 +40,12 @@ export const mockConfig = {
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
variant: 'sidebar',
- selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false,
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
+ attrWorkspacePath: 'test',
};
export const mockSuggestedColors = {
@@ -80,6 +80,7 @@ export const createLabelSuccessfulResponse = {
color: '#dc143c',
description: null,
title: 'ewrwrwer',
+ textColor: '#000000',
__typename: 'Label',
},
errors: [],
@@ -91,6 +92,7 @@ export const createLabelSuccessfulResponse = {
export const workspaceLabelsQueryResponse = {
data: {
workspace: {
+ id: 'gid://gitlab/Project/126',
labels: {
nodes: [
{
@@ -98,12 +100,14 @@ export const workspaceLabelsQueryResponse = {
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
+ textColor: '#000000',
},
{
color: '#2f7b2e',
description: null,
id: 'gid://gitlab/ProjectLabel/2',
title: 'Label2',
+ textColor: '#000000',
},
],
},
@@ -123,6 +127,7 @@ export const issuableLabelsQueryResponse = {
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
+ textColor: '#000000',
},
],
},
diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
index f1c3e8a1ddc..a6c9bda1aa2 100644
--- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -1,31 +1,45 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
-
-describe('toggleSidebar', () => {
- let vm;
- beforeEach(() => {
- const ToggleSidebar = Vue.extend(toggleSidebar);
- vm = mountComponent(ToggleSidebar, {
- collapsed: true,
+import { GlButton } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+
+import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+
+describe('ToggleSidebar', () => {
+ let wrapper;
+
+ const defaultProps = {
+ collapsed: true,
+ };
+
+ const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(ToggleSidebar, {
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
});
+ const findGlButton = () => wrapper.findComponent(GlButton);
+
it('should render the "chevron-double-lg-left" icon when collapsed', () => {
- expect(vm.$el.querySelector('[data-testid="chevron-double-lg-left-icon"]')).not.toBeNull();
+ createComponent();
+
+ expect(findGlButton().props('icon')).toBe('chevron-double-lg-left');
});
it('should render the "chevron-double-lg-right" icon when expanded', async () => {
- vm.collapsed = false;
- await Vue.nextTick();
- expect(vm.$el.querySelector('[data-testid="chevron-double-lg-right-icon"]')).not.toBeNull();
+ createComponent({ props: { collapsed: false } });
+
+ expect(findGlButton().props('icon')).toBe('chevron-double-lg-right');
});
- it('should emit toggle event when button clicked', () => {
- const toggle = jest.fn();
- vm.$on('toggle', toggle);
- vm.$el.click();
+ it('should emit toggle event when button clicked', async () => {
+ createComponent({ mountFn: mount });
+
+ findGlButton().trigger('click');
+ await wrapper.vm.$nextTick();
- expect(toggle).toHaveBeenCalled();
+ expect(wrapper.emitted('toggle')[0]).toBeDefined();
});
});
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
index a92f058f311..78abb89e7b8 100644
--- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -82,7 +82,7 @@ describe('User deletion obstacles list', () => {
createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
const msg = findObstacles().text();
- expect(msg).toContain(`in Project ${projectName}`);
+ expect(msg).toContain(`in project ${projectName}`);
expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
});
},
diff --git a/spec/frontend/whats_new/utils/notification_spec.js b/spec/frontend/whats_new/utils/notification_spec.js
index c361f934e59..ef61462a3c5 100644
--- a/spec/frontend/whats_new/utils/notification_spec.js
+++ b/spec/frontend/whats_new/utils/notification_spec.js
@@ -29,7 +29,7 @@ describe('~/whats_new/utils/notification', () => {
subject();
- expect(findNotificationCountEl()).toExist();
+ expect(findNotificationCountEl()).not.toBe(null);
expect(notificationEl.classList).toContain('with-notifications');
});
@@ -38,11 +38,11 @@ describe('~/whats_new/utils/notification', () => {
notificationEl.classList.add('with-notifications');
localStorage.setItem('display-whats-new-notification', 'version-digest');
- expect(findNotificationCountEl()).toExist();
+ expect(findNotificationCountEl()).not.toBe(null);
subject();
- expect(findNotificationCountEl()).not.toExist();
+ expect(findNotificationCountEl()).toBe(null);
expect(notificationEl.classList).not.toContain('with-notifications');
});
});
diff --git a/spec/frontend/work_items/components/app_spec.js b/spec/frontend/work_items/components/app_spec.js
new file mode 100644
index 00000000000..95034085493
--- /dev/null
+++ b/spec/frontend/work_items/components/app_spec.js
@@ -0,0 +1,24 @@
+import { shallowMount } from '@vue/test-utils';
+import App from '~/work_items/components/app.vue';
+
+describe('Work Items Application', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(App, {
+ stubs: {
+ 'router-view': true,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a component', () => {
+ createComponent();
+
+ expect(wrapper.exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
new file mode 100644
index 00000000000..efb4aa2feb2
--- /dev/null
+++ b/spec/frontend/work_items/mock_data.js
@@ -0,0 +1,17 @@
+export const workItemQueryResponse = {
+ workItem: {
+ __typename: 'WorkItem',
+ id: '1',
+ type: 'FEATURE',
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'TitleWidget',
+ type: 'TITLE',
+ contentText: 'Test',
+ },
+ ],
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
new file mode 100644
index 00000000000..64d02baed36
--- /dev/null
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -0,0 +1,70 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
+import { workItemQueryResponse } from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const WORK_ITEM_ID = '1';
+
+describe('Work items root component', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+
+ const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
+ fakeApollo = createMockApollo();
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: workItemQuery,
+ variables: {
+ id: WORK_ITEM_ID,
+ },
+ data: queryResponse,
+ });
+
+ wrapper = shallowMount(WorkItemsRoot, {
+ propsData: {
+ id: WORK_ITEM_ID,
+ },
+ localVue,
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('renders the title if title is in the widgets list', () => {
+ createComponent();
+
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toBe('Test');
+ });
+
+ it('does not render the title if title is not in the widgets list', () => {
+ const queryResponse = {
+ workItem: {
+ ...workItemQueryResponse.workItem,
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'SomeOtherWidget',
+ type: 'OTHER',
+ contentText: 'Test',
+ },
+ ],
+ },
+ },
+ };
+ createComponent({ queryResponse });
+
+ expect(findTitle().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
new file mode 100644
index 00000000000..0a57eab753f
--- /dev/null
+++ b/spec/frontend/work_items/router_spec.js
@@ -0,0 +1,30 @@
+import { mount } from '@vue/test-utils';
+import App from '~/work_items/components/app.vue';
+import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
+import { createRouter } from '~/work_items/router';
+
+describe('Work items router', () => {
+ let wrapper;
+
+ const createComponent = async (routeArg) => {
+ const router = createRouter('/work_item');
+ if (routeArg !== undefined) {
+ await router.push(routeArg);
+ }
+
+ wrapper = mount(App, {
+ router,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ window.location.hash = '';
+ });
+
+ it('renders work item on `/1` route', async () => {
+ await createComponent('/1');
+
+ expect(wrapper.find(WorkItemsRoot).exists()).toBe(true);
+ });
+});