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:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/.eslintrc.yml8
-rw-r--r--spec/frontend/__helpers__/fixtures.js5
-rw-r--r--spec/frontend/__helpers__/flush_promises.js3
-rw-r--r--spec/frontend/access_tokens/components/projects_token_selector_spec.js5
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js4
-rw-r--r--spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js4
-rw-r--r--spec/frontend/add_context_commits_modal/store/mutations_spec.js4
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js122
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js12
-rw-r--r--spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap4
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js24
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap2
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js139
-rw-r--r--spec/frontend/analytics/shared/utils_spec.js156
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js2
-rw-r--r--spec/frontend/blob/file_template_mediator_spec.js53
-rw-r--r--spec/frontend/boards/components/board_add_new_column_trigger_spec.js59
-rw-r--r--spec/frontend/boards/stores/actions_spec.js69
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js195
-rw-r--r--spec/frontend/clusters/agents/components/token_table_spec.js135
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap18
-rw-r--r--spec/frontend/clusters_list/components/agent_empty_state_spec.js77
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js117
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js246
-rw-r--r--spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js129
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js190
-rw-r--r--spec/frontend/clusters_list/components/mock_data.js12
-rw-r--r--spec/frontend/clusters_list/mocks/apollo.js45
-rw-r--r--spec/frontend/clusters_list/stubs.js14
-rw-r--r--spec/frontend/comment_type_toggle_spec.js169
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js4
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js4
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap9
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js1
-rw-r--r--spec/frontend/content_editor/components/wrappers/details_spec.js40
-rw-r--r--spec/frontend/content_editor/components/wrappers/frontmatter_spec.js43
-rw-r--r--spec/frontend/content_editor/extensions/color_chip_spec.js33
-rw-r--r--spec/frontend/content_editor/extensions/details_content_spec.js76
-rw-r--r--spec/frontend/content_editor/extensions/details_spec.js92
-rw-r--r--spec/frontend/content_editor/extensions/math_inline_spec.js42
-rw-r--r--spec/frontend/content_editor/extensions/table_of_contents_spec.js35
-rw-r--r--spec/frontend/content_editor/markdown_processing_examples.js2
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js107
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js8
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js36
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js37
-rw-r--r--spec/frontend/cycle_analytics/store/getters_spec.js27
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js29
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js16
-rw-r--r--spec/frontend/deploy_freeze/helpers.js5
-rw-r--r--spec/frontend/deploy_keys/components/action_btn_spec.js2
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js2
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js3
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js2
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js3
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap4
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js10
-rw-r--r--spec/frontend/design_management/utils/error_messages_spec.js31
-rw-r--r--spec/frontend/diffs/components/app_spec.js22
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js4
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js6
-rw-r--r--spec/frontend/diffs/mock_data/diff_with_commit.js5
-rw-r--r--spec/frontend/diffs/store/actions_spec.js4
-rw-r--r--spec/frontend/diffs/utils/tree_worker_utils_spec.js (renamed from spec/frontend/diffs/utils/workers_spec.js)8
-rw-r--r--spec/frontend/editor/source_editor_ci_schema_ext_spec.js6
-rw-r--r--spec/frontend/environments/environment_delete_spec.js13
-rw-r--r--spec/frontend/environments/environment_monitoring_spec.js22
-rw-r--r--spec/frontend/environments/environment_pin_spec.js10
-rw-r--r--spec/frontend/environments/environment_rollback_spec.js14
-rw-r--r--spec/frontend/environments/environment_terminal_button_spec.js19
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js47
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js43
-rw-r--r--spec/frontend/experimentation/utils_spec.js14
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js16
-rw-r--r--spec/frontend/filterable_list_spec.js2
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js5
-rw-r--r--spec/frontend/filtered_search/droplab/constants_spec.js (renamed from spec/frontend/droplab/constants_spec.js)2
-rw-r--r--spec/frontend/filtered_search/droplab/drop_down_spec.js (renamed from spec/frontend/droplab/drop_down_spec.js)6
-rw-r--r--spec/frontend/filtered_search/droplab/hook_spec.js (renamed from spec/frontend/droplab/hook_spec.js)6
-rw-r--r--spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js (renamed from spec/frontend/droplab/plugins/ajax_filter_spec.js)2
-rw-r--r--spec/frontend/filtered_search/droplab/plugins/ajax_spec.js (renamed from spec/frontend/droplab/plugins/ajax_spec.js)2
-rw-r--r--spec/frontend/filtered_search/droplab/plugins/input_setter_spec.js (renamed from spec/frontend/droplab/plugins/input_setter_spec.js)2
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js8
-rw-r--r--spec/frontend/fixtures/abuse_reports.rb4
-rw-r--r--spec/frontend/fixtures/admin_users.rb4
-rw-r--r--spec/frontend/fixtures/analytics.rb10
-rw-r--r--spec/frontend/fixtures/api_markdown.rb6
-rw-r--r--spec/frontend/fixtures/api_markdown.yml83
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb4
-rw-r--r--spec/frontend/fixtures/api_projects.rb4
-rw-r--r--spec/frontend/fixtures/application_settings.rb4
-rw-r--r--spec/frontend/fixtures/autocomplete.rb4
-rw-r--r--spec/frontend/fixtures/autocomplete_sources.rb4
-rw-r--r--spec/frontend/fixtures/blob.rb4
-rw-r--r--spec/frontend/fixtures/branches.rb5
-rw-r--r--spec/frontend/fixtures/clusters.rb4
-rw-r--r--spec/frontend/fixtures/commit.rb5
-rw-r--r--spec/frontend/fixtures/deploy_keys.rb4
-rw-r--r--spec/frontend/fixtures/freeze_period.rb4
-rw-r--r--spec/frontend/fixtures/groups.rb4
-rw-r--r--spec/frontend/fixtures/issues.rb4
-rw-r--r--spec/frontend/fixtures/jobs.rb4
-rw-r--r--spec/frontend/fixtures/labels.rb4
-rw-r--r--spec/frontend/fixtures/merge_requests.rb4
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb4
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb4
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb4
-rw-r--r--spec/frontend/fixtures/pipelines.rb4
-rw-r--r--spec/frontend/fixtures/projects.rb8
-rw-r--r--spec/frontend/fixtures/projects_json.rb4
-rw-r--r--spec/frontend/fixtures/prometheus_service.rb4
-rw-r--r--spec/frontend/fixtures/raw.rb8
-rw-r--r--spec/frontend/fixtures/releases.rb8
-rw-r--r--spec/frontend/fixtures/runner.rb4
-rw-r--r--spec/frontend/fixtures/search.rb4
-rw-r--r--spec/frontend/fixtures/services.rb4
-rw-r--r--spec/frontend/fixtures/sessions.rb4
-rw-r--r--spec/frontend/fixtures/snippet.rb4
-rw-r--r--spec/frontend/fixtures/startup_css.rb8
-rw-r--r--spec/frontend/fixtures/static/oauth_remember_me.html33
-rw-r--r--spec/frontend/fixtures/tags.rb4
-rw-r--r--spec/frontend/fixtures/timezones.rb4
-rw-r--r--spec/frontend/fixtures/todos.rb4
-rw-r--r--spec/frontend/fixtures/u2f.rb4
-rw-r--r--spec/frontend/fixtures/webauthn.rb4
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js4
-rw-r--r--spec/frontend/header_search/components/app_spec.js32
-rw-r--r--spec/frontend/header_search/components/header_search_autocomplete_items_spec.js108
-rw-r--r--spec/frontend/header_search/mock_data.js69
-rw-r--r--spec/frontend/header_search/store/actions_spec.js34
-rw-r--r--spec/frontend/header_search/store/getters_spec.js40
-rw-r--r--spec/frontend/header_search/store/mutations_spec.js29
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js24
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js2
-rw-r--r--spec/frontend/ide/stores/utils_spec.js2
-rw-r--r--spec/frontend/import_entities/components/pagination_bar_spec.js92
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js3
-rw-r--r--spec/frontend/integrations/edit/components/override_dropdown_spec.js2
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js12
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js29
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js4
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js14
-rw-r--r--spec/frontend/invite_members/utils/response_message_parser_spec.js28
-rw-r--r--spec/frontend/issuable/components/csv_export_modal_spec.js5
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js7
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js7
-rw-r--r--spec/frontend/issuable_form_spec.js19
-rw-r--r--spec/frontend/issuable_list/components/issuable_item_spec.js15
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js63
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js36
-rw-r--r--spec/frontend/issues_list/components/new_issue_dropdown_spec.js131
-rw-r--r--spec/frontend/issues_list/mock_data.js34
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap32
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js35
-rw-r--r--spec/frontend/jobs/components/job_container_item_spec.js2
-rw-r--r--spec/frontend/jobs/components/job_log_controllers_spec.js14
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js8
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js12
-rw-r--r--spec/frontend/jobs/mixins/delayed_job_mixin_spec.js2
-rw-r--r--spec/frontend/jobs/store/actions_spec.js112
-rw-r--r--spec/frontend/jobs/store/getters_spec.js8
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js80
-rw-r--r--spec/frontend/jobs/store/utils_spec.js12
-rw-r--r--spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js155
-rw-r--r--spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap21
-rw-r--r--spec/frontend/lib/logger/hello_spec.js28
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js18
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js15
-rw-r--r--spec/frontend/lib/utils/is_navigating_away_spec.js23
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js21
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js35
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js5
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js29
-rw-r--r--spec/frontend/members/components/modals/remove_member_modal_spec.js33
-rw-r--r--spec/frontend/members/components/table/expires_at_spec.js86
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js88
-rw-r--r--spec/frontend/members/mock_data.js7
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/fixture_data.js6
-rw-r--r--spec/frontend/namespace_select_spec.js65
-rw-r--r--spec/frontend/notebook/cells/code_spec.js5
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js9
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js7
-rw-r--r--spec/frontend/notebook/index_spec.js9
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js14
-rw-r--r--spec/frontend/notes/components/diff_with_note_spec.js9
-rw-r--r--spec/frontend/notes/components/note_form_spec.js10
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js7
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js21
-rw-r--r--spec/frontend/notes/stores/getters_spec.js5
-rw-r--r--spec/frontend/oauth_remember_me_spec.js2
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap36
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap34
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap112
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap36
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap36
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap168
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap45
-rw-r--r--spec/frontend/packages/details/components/additional_metadata_spec.js119
-rw-r--r--spec/frontend/packages/details/components/composer_installation_spec.js133
-rw-r--r--spec/frontend/packages/details/components/conan_installation_spec.js72
-rw-r--r--spec/frontend/packages/details/components/dependency_row_spec.js62
-rw-r--r--spec/frontend/packages/details/components/installation_title_spec.js58
-rw-r--r--spec/frontend/packages/details/components/installations_commands_spec.js61
-rw-r--r--spec/frontend/packages/details/components/maven_installation_spec.js184
-rw-r--r--spec/frontend/packages/details/components/npm_installation_spec.js123
-rw-r--r--spec/frontend/packages/details/components/nuget_installation_spec.js79
-rw-r--r--spec/frontend/packages/details/components/package_title_spec.js189
-rw-r--r--spec/frontend/packages/details/components/pypi_installation_spec.js72
-rw-r--r--spec/frontend/packages/details/store/getters_spec.js295
-rw-r--r--spec/frontend/packages/shared/components/package_list_row_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js173
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/mock_data.js21
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap (renamed from spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap (renamed from spec/frontend/packages_and_registries/infrastructure_registry/components/__snapshots__/terraform_installation_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js (renamed from spec/frontend/packages/details/components/app_spec.js)104
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js (renamed from spec/frontend/packages_and_registries/infrastructure_registry/components/details_title_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js (renamed from spec/frontend/packages/details/components/file_sha_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js (renamed from spec/frontend/packages/details/components/package_files_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js (renamed from spec/frontend/packages/details/components/package_history_spec.js)6
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js (renamed from spec/frontend/packages_and_registries/infrastructure_registry/components/terraform_installation_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/mock_data.js (renamed from spec/frontend/packages/details/mock_data.js)0
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js (renamed from spec/frontend/packages/details/store/actions_spec.js)8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js40
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/mutations_spec.js (renamed from spec/frontend/packages/details/store/mutations_spec.js)6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js29
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js62
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap12
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap122
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap68
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap42
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js154
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js156
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js273
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js145
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js47
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js34
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js189
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js322
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js277
-rw-r--r--spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js66
-rw-r--r--spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js50
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap2
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js93
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js175
-rw-r--r--spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js92
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap2
-rw-r--r--spec/frontend/pages/projects/new/components/new_project_url_select_spec.js122
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js78
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/editor/text_editor_spec.js5
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js51
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js42
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js21
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js53
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js35
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js9
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js27
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js60
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js3
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js3
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js4
-rw-r--r--spec/frontend/projects/new/components/app_spec.js (renamed from spec/frontend/pages/projects/new/components/app_spec.js)2
-rw-r--r--spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js (renamed from spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js)2
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js235
-rw-r--r--spec/frontend/projects/projects_filterable_list_spec.js2
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js345
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js11
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js72
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js7
-rw-r--r--spec/frontend/related_merge_requests/components/related_merge_requests_spec.js6
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js3
-rw-r--r--spec/frontend/releases/components/app_index_apollo_client_spec.js4
-rw-r--r--spec/frontend/releases/components/app_show_spec.js6
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js4
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_spec.js4
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js6
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js4
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js6
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js8
-rw-r--r--spec/frontend/releases/util_spec.js14
-rw-r--r--spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js6
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js10
-rw-r--r--spec/frontend/reports/codequality_report/store/getters_spec.js6
-rw-r--r--spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js12
-rw-r--r--spec/frontend/reports/components/report_section_spec.js12
-rw-r--r--spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js18
-rw-r--r--spec/frontend/repository/commits_service_spec.js84
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js109
-rw-r--r--spec/frontend/repository/components/blob_edit_spec.js6
-rw-r--r--spec/frontend/repository/components/blob_viewers/video_viewer_spec.js22
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js38
-rw-r--r--spec/frontend/repository/components/fork_suggestion_spec.js44
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js203
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap75
-rw-r--r--spec/frontend/repository/components/table/index_spec.js33
-rw-r--r--spec/frontend/repository/components/table/row_spec.js34
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js22
-rw-r--r--spec/frontend/repository/router_spec.js28
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js22
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js14
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_name_cell_spec.js)27
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js17
-rw-r--r--spec/frontend/runner/components/runner_state_locked_badge_spec.js45
-rw-r--r--spec/frontend/runner/components/runner_state_paused_badge_spec.js45
-rw-r--r--spec/frontend/runner/components/runner_type_badge_spec.js23
-rw-r--r--spec/frontend/runner/components/runner_type_help_spec.js32
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js28
-rw-r--r--spec/frontend/runner/mock_data.js22
-rw-r--r--spec/frontend/search_settings/components/search_settings_spec.js35
-rw-r--r--spec/frontend/sidebar/assignees_spec.js8
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js20
-rw-r--r--spec/frontend/sidebar/sidebar_labels_spec.js4
-rw-r--r--spec/frontend/sidebar/todo_spec.js2
-rw-r--r--spec/frontend/snippets/components/show_spec.js18
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js41
-rw-r--r--spec/frontend/test_setup.js3
-rw-r--r--spec/frontend/tracking/get_standard_context_spec.js29
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js140
-rw-r--r--spec/frontend/tracking/tracking_spec.js (renamed from spec/frontend/tracking_spec.js)258
-rw-r--r--spec/frontend/tracking/utils_spec.js99
-rw-r--r--spec/frontend/users_select/test_helper.js5
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/actions_spec.js35
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/index_spec.js7
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js36
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js25
-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_ready_to_merge_spec.js10
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js9
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js2
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js18
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js51
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/test_extension.js37
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js141
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js57
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js144
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js109
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js24
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap11
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js116
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js22
-rw-r--r--spec/frontend/vue_shared/directives/validation_spec.js137
-rw-r--r--spec/frontend/vue_shared/oncall_schedules_list_spec.js87
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js12
-rw-r--r--spec/frontend/vue_shared/security_reports/store/getters_spec.js4
393 files changed, 9417 insertions, 5159 deletions
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index 145e6c8961a..e12c4e5e820 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -12,7 +12,6 @@ settings:
jest:
jestConfigFile: 'jest.config.js'
globals:
- getJSONFixture: false
loadFixtures: false
setFixtures: false
rules:
@@ -26,4 +25,9 @@ rules:
- off
"@gitlab/no-global-event-off":
- off
-
+ import/no-unresolved:
+ - error
+ # The test fixtures and graphql schema are dynamically generated in CI
+ # during the `frontend-fixtures` and `graphql-schema-dump` jobs.
+ # They may not be present during linting.
+ - ignore: ['^test_fixtures\/', 'tmp/tests/graphql/gitlab_schema.graphql']
diff --git a/spec/frontend/__helpers__/fixtures.js b/spec/frontend/__helpers__/fixtures.js
index 4b86724df93..d8054d32fae 100644
--- a/spec/frontend/__helpers__/fixtures.js
+++ b/spec/frontend/__helpers__/fixtures.js
@@ -20,6 +20,11 @@ Did you run bin/rake frontend:fixtures?`,
return fs.readFileSync(absolutePath, 'utf8');
}
+/**
+ * @deprecated Use `import` to load a JSON fixture instead.
+ * See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#use-fixtures,
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/339346.
+ */
export const getJSONFixture = (relativePath) => JSON.parse(getFixture(relativePath));
export const resetHTMLFixture = () => {
diff --git a/spec/frontend/__helpers__/flush_promises.js b/spec/frontend/__helpers__/flush_promises.js
new file mode 100644
index 00000000000..5287a060753
--- /dev/null
+++ b/spec/frontend/__helpers__/flush_promises.js
@@ -0,0 +1,3 @@
+export default function flushPromises() {
+ return new Promise(setImmediate);
+}
diff --git a/spec/frontend/access_tokens/components/projects_token_selector_spec.js b/spec/frontend/access_tokens/components/projects_token_selector_spec.js
index 09f52fe9a5f..40aaf16d41f 100644
--- a/spec/frontend/access_tokens/components/projects_token_selector_spec.js
+++ b/spec/frontend/access_tokens/components/projects_token_selector_spec.js
@@ -11,7 +11,7 @@ import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { getJSONFixture } from 'helpers/fixtures';
+import getProjectsQueryResponse from 'test_fixtures/graphql/projects/access_tokens/get_projects.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -20,9 +20,6 @@ import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
describe('ProjectsTokenSelector', () => {
- const getProjectsQueryResponse = getJSONFixture(
- 'graphql/projects/access_tokens/get_projects.query.graphql.json',
- );
const getProjectsQueryResponsePage2 = produce(
getProjectsQueryResponse,
(getProjectsQueryResponseDraft) => {
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index 2832de98769..e7a20ae114c 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -1,12 +1,12 @@
import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
import * as actions from '~/add_context_commits_modal/store/actions';
import mutations from '~/add_context_commits_modal/store/mutations';
import defaultState from '~/add_context_commits_modal/store/state';
-import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -18,7 +18,7 @@ describe('AddContextCommitsModal', () => {
const removeContextCommits = jest.fn();
const resetModalState = jest.fn();
const searchCommits = jest.fn();
- const { commit } = getDiffWithCommit();
+ const { commit } = getDiffWithCommit;
const createWrapper = (props = {}) => {
store = new Vuex.Store({
diff --git a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
index 75f1cc41e23..85ecb4313c2 100644
--- a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
@@ -1,12 +1,12 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
import CommitItem from '~/diffs/components/commit_item.vue';
-import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
describe('ReviewTabContainer', () => {
let wrapper;
- const { commit } = getDiffWithCommit();
+ const { commit } = getDiffWithCommit;
const createWrapper = (props = {}) => {
wrapper = shallowMount(ReviewTabContainer, {
diff --git a/spec/frontend/add_context_commits_modal/store/mutations_spec.js b/spec/frontend/add_context_commits_modal/store/mutations_spec.js
index 2331a4af1bc..7517c1c391e 100644
--- a/spec/frontend/add_context_commits_modal/store/mutations_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/mutations_spec.js
@@ -1,10 +1,10 @@
+import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import { TEST_HOST } from 'helpers/test_constants';
import * as types from '~/add_context_commits_modal/store/mutation_types';
import mutations from '~/add_context_commits_modal/store/mutations';
-import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
describe('AddContextCommitsModalStoreMutations', () => {
- const { commit } = getDiffWithCommit();
+ const { commit } = getDiffWithCommit;
describe('SET_BASE_CONFIG', () => {
it('should set contextCommitsPath, mergeRequestIid and projectId', () => {
const state = {};
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
index 4bb22feb913..5b4f954b672 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -35,9 +35,6 @@ describe('Signup Form', () => {
const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group');
const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group');
-
- const findRequireAdminApprovalCheckbox = () =>
- wrapper.findByTestId('require-admin-approval-checkbox');
const findUserCapInput = () => wrapper.findByTestId('user-cap-input');
const findModal = () => wrapper.find(GlModal);
@@ -191,125 +188,6 @@ describe('Signup Form', () => {
});
describe('form submit button confirmation modal for side-effect of adding possibly unwanted new users', () => {
- it.each`
- requireAdminApprovalAction | userCapAction | pendingUserCount | buttonEffect
- ${'unchanged from true'} | ${'unchanged'} | ${0} | ${'submits form'}
- ${'unchanged from false'} | ${'unchanged'} | ${0} | ${'submits form'}
- ${'toggled off'} | ${'unchanged'} | ${1} | ${'shows confirmation modal'}
- ${'toggled off'} | ${'unchanged'} | ${0} | ${'submits form'}
- ${'toggled on'} | ${'unchanged'} | ${0} | ${'submits form'}
- ${'unchanged from false'} | ${'increased'} | ${1} | ${'shows confirmation modal'}
- ${'unchanged from true'} | ${'increased'} | ${0} | ${'submits form'}
- ${'toggled off'} | ${'increased'} | ${1} | ${'shows confirmation modal'}
- ${'toggled off'} | ${'increased'} | ${0} | ${'submits form'}
- ${'toggled on'} | ${'increased'} | ${1} | ${'shows confirmation modal'}
- ${'toggled on'} | ${'increased'} | ${0} | ${'submits form'}
- ${'toggled on'} | ${'decreased'} | ${0} | ${'submits form'}
- ${'toggled on'} | ${'decreased'} | ${1} | ${'submits form'}
- ${'unchanged from false'} | ${'changed from limited to unlimited'} | ${1} | ${'shows confirmation modal'}
- ${'unchanged from false'} | ${'changed from limited to unlimited'} | ${0} | ${'submits form'}
- ${'unchanged from false'} | ${'changed from unlimited to limited'} | ${0} | ${'submits form'}
- ${'unchanged from false'} | ${'unchanged from unlimited'} | ${0} | ${'submits form'}
- `(
- '$buttonEffect if require admin approval for new sign-ups is $requireAdminApprovalAction and the user cap is $userCapAction and pending user count is $pendingUserCount',
- async ({ requireAdminApprovalAction, userCapAction, pendingUserCount, buttonEffect }) => {
- let isModalDisplayed;
-
- switch (buttonEffect) {
- case 'shows confirmation modal':
- isModalDisplayed = true;
- break;
- case 'submits form':
- isModalDisplayed = false;
- break;
- default:
- isModalDisplayed = false;
- break;
- }
-
- const isFormSubmittedWhenClickingFormSubmitButton = !isModalDisplayed;
-
- const injectedProps = {
- pendingUserCount,
- };
-
- const USER_CAP_DEFAULT = 5;
-
- switch (userCapAction) {
- case 'changed from unlimited to limited':
- injectedProps.newUserSignupsCap = '';
- break;
- case 'unchanged from unlimited':
- injectedProps.newUserSignupsCap = '';
- break;
- default:
- injectedProps.newUserSignupsCap = USER_CAP_DEFAULT;
- break;
- }
-
- switch (requireAdminApprovalAction) {
- case 'unchanged from true':
- injectedProps.requireAdminApprovalAfterUserSignup = true;
- break;
- case 'unchanged from false':
- injectedProps.requireAdminApprovalAfterUserSignup = false;
- break;
- case 'toggled off':
- injectedProps.requireAdminApprovalAfterUserSignup = true;
- break;
- case 'toggled on':
- injectedProps.requireAdminApprovalAfterUserSignup = false;
- break;
- default:
- injectedProps.requireAdminApprovalAfterUserSignup = false;
- break;
- }
-
- formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
-
- await mountComponent({
- injectedProps,
- stubs: { GlButton, GlModal: stubComponent(GlModal) },
- });
-
- findModal().vm.show = jest.fn();
-
- if (
- requireAdminApprovalAction === 'toggled off' ||
- requireAdminApprovalAction === 'toggled on'
- ) {
- await findRequireAdminApprovalCheckbox().vm.$emit('input', false);
- }
-
- switch (userCapAction) {
- case 'increased':
- await findUserCapInput().vm.$emit('input', USER_CAP_DEFAULT + 1);
- break;
- case 'decreased':
- await findUserCapInput().vm.$emit('input', USER_CAP_DEFAULT - 1);
- break;
- case 'changed from limited to unlimited':
- await findUserCapInput().vm.$emit('input', '');
- break;
- case 'changed from unlimited to limited':
- await findUserCapInput().vm.$emit('input', USER_CAP_DEFAULT);
- break;
- default:
- break;
- }
-
- await findFormSubmitButton().trigger('click');
-
- if (isFormSubmittedWhenClickingFormSubmitButton) {
- expect(formSubmitSpy).toHaveBeenCalled();
- expect(findModal().vm.show).not.toHaveBeenCalled();
- } else {
- expect(formSubmitSpy).not.toHaveBeenCalled();
- expect(findModal().vm.show).toHaveBeenCalled();
- }
- },
- );
-
describe('modal actions', () => {
beforeEach(async () => {
const INITIAL_USER_CAP = 5;
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index fd05b08a3fb..67dcf5c6149 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -5,6 +5,7 @@ import { nextTick } from 'vue';
import Actions from '~/admin/users/components/actions';
import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
import { paths } from '../../mock_data';
@@ -46,7 +47,10 @@ describe('Action components', () => {
});
describe('DELETE_ACTION_COMPONENTS', () => {
- const oncallSchedules = [{ name: 'schedule1' }, { name: 'schedule2' }];
+ const userDeletionObstacles = [
+ { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
+ { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
+ ];
it.each(DELETE_ACTIONS.map((action) => [action, paths[action]]))(
'renders a dropdown item for "%s"',
@@ -56,7 +60,7 @@ describe('Action components', () => {
props: {
username: 'John Doe',
paths,
- oncallSchedules,
+ userDeletionObstacles,
},
stubs: { SharedDeleteAction },
});
@@ -69,8 +73,8 @@ describe('Action components', () => {
expect(sharedAction.attributes('data-delete-user-url')).toBe(expectedPath);
expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action));
expect(sharedAction.attributes('data-username')).toBe('John Doe');
- expect(sharedAction.attributes('data-oncall-schedules')).toBe(
- JSON.stringify(oncallSchedules),
+ expect(sharedAction.attributes('data-user-deletion-obstacles')).toBe(
+ JSON.stringify(userDeletionObstacles),
);
expect(findDropdownItem().exists()).toBe(true);
},
diff --git a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
index 5e367891337..472158a9b10 100644
--- a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
@@ -8,8 +8,8 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
/>
</p>
- <oncall-schedules-list-stub
- schedules="schedule1,schedule2"
+ <user-deletion-obstacles-list-stub
+ obstacles="schedule1,policy1"
username="username"
/>
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index fee74764645..82307c9e3b3 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
import ModalStub from './stubs/modal_stub';
const TEST_DELETE_USER_URL = 'delete-url';
@@ -25,7 +25,7 @@ describe('User Operation confirmation modal', () => {
const getUsername = () => findUsernameInput().attributes('value');
const getMethodParam = () => new FormData(findForm().element).get('_method');
const getFormAction = () => findForm().attributes('action');
- const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList);
+ const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
const setUsername = (username) => {
findUsernameInput().vm.$emit('input', username);
@@ -33,7 +33,7 @@ describe('User Operation confirmation modal', () => {
const username = 'username';
const badUsername = 'bad_username';
- const oncallSchedules = '["schedule1", "schedule2"]';
+ const userDeletionObstacles = '["schedule1", "policy1"]';
const createComponent = (props = {}) => {
wrapper = shallowMount(DeleteUserModal, {
@@ -46,7 +46,7 @@ describe('User Operation confirmation modal', () => {
deleteUserUrl: TEST_DELETE_USER_URL,
blockUserUrl: TEST_BLOCK_USER_URL,
csrfToken: TEST_CSRF,
- oncallSchedules,
+ userDeletionObstacles,
...props,
},
stubs: {
@@ -150,18 +150,18 @@ describe('User Operation confirmation modal', () => {
});
});
- describe('Related oncall-schedules list', () => {
- it('does NOT render the list when user has no related schedules', () => {
- createComponent({ oncallSchedules: '[]' });
- expect(findOnCallSchedulesList().exists()).toBe(false);
+ describe('Related user-deletion-obstacles list', () => {
+ it('does NOT render the list when user has no related obstacles', () => {
+ createComponent({ userDeletionObstacles: '[]' });
+ expect(findUserDeletionObstaclesList().exists()).toBe(false);
});
- it('renders the list when user has related schedules', () => {
+ it('renders the list when user has related obstalces', () => {
createComponent();
- const schedules = findOnCallSchedulesList();
- expect(schedules.exists()).toBe(true);
- expect(schedules.props('schedules')).toEqual(JSON.parse(oncallSchedules));
+ const obstacles = findUserDeletionObstaclesList();
+ expect(obstacles.exists()).toBe(true);
+ expect(obstacles.props('obstacles')).toEqual(JSON.parse(userDeletionObstacles));
});
});
});
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
index ddb188edb10..f4d3fd97fd8 100644
--- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -52,13 +52,13 @@ exports[`Alert integration settings form default state should match the default
block="true"
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
data-qa-selector="incident_templates_dropdown"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
id="alert-integration-settings-issue-template"
- showhighlighteditemstitle="true"
size="medium"
text="selecte_tmpl"
variant="default"
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 2537b8fb816..5d681c7da4f 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -1,6 +1,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import getProjects from '~/analytics/shared/graphql/projects.query.graphql';
@@ -25,6 +27,17 @@ const projects = [
},
];
+const MockGlDropdown = stubComponent(GlDropdown, {
+ template: `
+ <div>
+ <div data-testid="vsa-highlighted-items">
+ <slot name="highlighted-items"></slot>
+ </div>
+ <div data-testid="vsa-default-items"><slot></slot></div>
+ </div>
+ `,
+});
+
const defaultMocks = {
$apollo: {
query: jest.fn().mockResolvedValue({
@@ -38,22 +51,33 @@ let spyQuery;
describe('ProjectsDropdownFilter component', () => {
let wrapper;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, stubs = {}) => {
spyQuery = defaultMocks.$apollo.query;
- wrapper = mount(ProjectsDropdownFilter, {
+ wrapper = mountExtended(ProjectsDropdownFilter, {
mocks: { ...defaultMocks },
propsData: {
groupId: 1,
groupNamespace: 'gitlab-org',
...props,
},
+ stubs,
});
};
+ const createWithMockDropdown = (props) => {
+ createComponent(props, { GlDropdown: MockGlDropdown });
+ return waitForPromises();
+ };
+
afterEach(() => {
wrapper.destroy();
});
+ const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items');
+ const findUnhighlightedItems = () => wrapper.findByTestId('vsa-default-items');
+ const findHighlightedItemsTitle = () => wrapper.findByText('Selected');
+ const findClearAllButton = () => wrapper.findByText('Clear all');
+
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () =>
@@ -75,8 +99,19 @@ describe('ProjectsDropdownFilter component', () => {
const findDropdownFullPathAtIndex = (index) =>
findDropdownAtIndex(index).find('[data-testid="project-full-path"]');
- const selectDropdownItemAtIndex = (index) =>
+ const selectDropdownItemAtIndex = (index) => {
findDropdownAtIndex(index).find('button').trigger('click');
+ return wrapper.vm.$nextTick();
+ };
+
+ // NOTE: Selected items are now visually separated from unselected items
+ const findSelectedDropdownItems = () => findHighlightedItems().findAll(GlDropdownItem);
+
+ const findSelectedDropdownAtIndex = (index) => findSelectedDropdownItems().at(index);
+ const findSelectedButtonIdentIconAtIndex = (index) =>
+ findSelectedDropdownAtIndex(index).find('div.gl-avatar-identicon');
+ const findSelectedButtonAvatarItemAtIndex = (index) =>
+ findSelectedDropdownAtIndex(index).find('img.gl-avatar');
const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id);
@@ -109,7 +144,80 @@ describe('ProjectsDropdownFilter component', () => {
});
});
- describe('when passed a an array of defaultProject as prop', () => {
+ describe('highlighted items', () => {
+ const blockDefaultProps = { multiSelect: true };
+ beforeEach(() => {
+ createComponent(blockDefaultProps);
+ });
+
+ describe('with no project selected', () => {
+ it('does not render the highlighted items', async () => {
+ await createWithMockDropdown(blockDefaultProps);
+ expect(findSelectedDropdownItems().length).toBe(0);
+ });
+
+ it('does not render the highlighted items title', () => {
+ expect(findHighlightedItemsTitle().exists()).toBe(false);
+ });
+
+ it('does not render the clear all button', () => {
+ expect(findClearAllButton().exists()).toBe(false);
+ });
+ });
+
+ describe('with a selected project', () => {
+ beforeEach(async () => {
+ await selectDropdownItemAtIndex(0);
+ });
+
+ it('renders the highlighted items', async () => {
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(0);
+
+ expect(findSelectedDropdownItems().length).toBe(1);
+ });
+
+ it('renders the highlighted items title', () => {
+ expect(findHighlightedItemsTitle().exists()).toBe(true);
+ });
+
+ it('renders the clear all button', () => {
+ expect(findClearAllButton().exists()).toBe(true);
+ });
+
+ it('clears all selected items when the clear all button is clicked', async () => {
+ await selectDropdownItemAtIndex(1);
+
+ expect(wrapper.text()).toContain('2 projects selected');
+
+ findClearAllButton().trigger('click');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.text()).not.toContain('2 projects selected');
+ expect(wrapper.text()).toContain('Select projects');
+ });
+ });
+ });
+
+ describe('with a selected project and search term', () => {
+ beforeEach(async () => {
+ await createWithMockDropdown({ multiSelect: true });
+
+ selectDropdownItemAtIndex(0);
+ wrapper.setData({ searchTerm: 'this is a very long search string' });
+ });
+
+ it('renders the highlighted items', async () => {
+ expect(findUnhighlightedItems().findAll('li').length).toBe(1);
+ });
+
+ it('hides the unhighlighted items that do not match the string', async () => {
+ expect(findUnhighlightedItems().findAll('li').length).toBe(1);
+ expect(findUnhighlightedItems().text()).toContain('No matching results');
+ });
+ });
+
+ describe('when passed an array of defaultProject as prop', () => {
beforeEach(() => {
createComponent({
defaultProjects: [projects[0]],
@@ -130,8 +238,9 @@ describe('ProjectsDropdownFilter component', () => {
});
describe('when multiSelect is false', () => {
+ const blockDefaultProps = { multiSelect: false };
beforeEach(() => {
- createComponent({ multiSelect: false });
+ createComponent(blockDefaultProps);
});
describe('displays the correct information', () => {
@@ -183,21 +292,19 @@ describe('ProjectsDropdownFilter component', () => {
});
it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => {
- selectDropdownItemAtIndex(0);
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(0);
- await wrapper.vm.$nextTick().then(() => {
- expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
- expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
- });
+ expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(true);
+ expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(false);
});
it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => {
- selectDropdownItemAtIndex(1);
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(1);
- await wrapper.vm.$nextTick().then(() => {
- expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
- expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
- });
+ expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(false);
+ expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js
index e3293f2d8bd..0513ccb2890 100644
--- a/spec/frontend/analytics/shared/utils_spec.js
+++ b/spec/frontend/analytics/shared/utils_spec.js
@@ -1,4 +1,10 @@
-import { filterBySearchTerm } from '~/analytics/shared/utils';
+import {
+ filterBySearchTerm,
+ extractFilterQueryParameters,
+ extractPaginationQueryParameters,
+ getDataZoomOption,
+} from '~/analytics/shared/utils';
+import { objectToQuery } from '~/lib/utils/url_utility';
describe('filterBySearchTerm', () => {
const data = [
@@ -22,3 +28,151 @@ describe('filterBySearchTerm', () => {
expect(filterBySearchTerm(data, 'ne', 'title')).toEqual([data[0]]);
});
});
+
+describe('extractFilterQueryParameters', () => {
+ const selectedAuthor = 'Author 1';
+ const selectedMilestone = 'Milestone 1.0';
+ const selectedSourceBranch = 'main';
+ const selectedTargetBranch = 'feature-1';
+ const selectedAssigneeList = ['Alice', 'Bob'];
+ const selectedLabelList = ['Label 1', 'Label 2'];
+
+ const queryParamsString = objectToQuery({
+ source_branch_name: selectedSourceBranch,
+ target_branch_name: selectedTargetBranch,
+ author_username: selectedAuthor,
+ milestone_title: selectedMilestone,
+ assignee_username: selectedAssigneeList,
+ label_name: selectedLabelList,
+ });
+
+ it('extracts the correct filter parameters from a url', () => {
+ const result = extractFilterQueryParameters(queryParamsString);
+ const operator = '=';
+ const expectedFilters = {
+ selectedAssigneeList: { operator, value: selectedAssigneeList.join(',') },
+ selectedLabelList: { operator, value: selectedLabelList.join(',') },
+ selectedAuthor: { operator, value: selectedAuthor },
+ selectedMilestone: { operator, value: selectedMilestone },
+ selectedSourceBranch: { operator, value: selectedSourceBranch },
+ selectedTargetBranch: { operator, value: selectedTargetBranch },
+ };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('returns null for missing parameters', () => {
+ const result = extractFilterQueryParameters('');
+ const expectedFilters = {
+ selectedAuthor: null,
+ selectedMilestone: null,
+ selectedSourceBranch: null,
+ selectedTargetBranch: null,
+ };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('only returns the parameters we expect', () => {
+ const result = extractFilterQueryParameters('foo="one"&bar="two"');
+ const resultKeys = Object.keys(result);
+ ['foo', 'bar'].forEach((key) => {
+ expect(resultKeys).not.toContain(key);
+ });
+
+ [
+ 'selectedAuthor',
+ 'selectedMilestone',
+ 'selectedSourceBranch',
+ 'selectedTargetBranch',
+ 'selectedAssigneeList',
+ 'selectedLabelList',
+ ].forEach((key) => {
+ expect(resultKeys).toContain(key);
+ });
+ });
+
+ it('returns an empty array for missing list parameters', () => {
+ const result = extractFilterQueryParameters('');
+ const expectedFilters = { selectedAssigneeList: [], selectedLabelList: [] };
+ expect(result).toMatchObject(expectedFilters);
+ });
+});
+
+describe('extractPaginationQueryParameters', () => {
+ const sort = 'title';
+ const direction = 'asc';
+ const page = '1';
+ const queryParamsString = objectToQuery({ sort, direction, page });
+
+ it('extracts the correct filter parameters from a url', () => {
+ const result = extractPaginationQueryParameters(queryParamsString);
+ const expectedFilters = { sort, page, direction };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('returns null for missing parameters', () => {
+ const result = extractPaginationQueryParameters('');
+ const expectedFilters = { sort: null, direction: null, page: null };
+ expect(result).toMatchObject(expectedFilters);
+ });
+
+ it('only returns the parameters we expect', () => {
+ const result = extractPaginationQueryParameters('foo="one"&bar="two"&qux="three"');
+ const resultKeys = Object.keys(result);
+ ['foo', 'bar', 'qux'].forEach((key) => {
+ expect(resultKeys).not.toContain(key);
+ });
+
+ ['sort', 'page', 'direction'].forEach((key) => {
+ expect(resultKeys).toContain(key);
+ });
+ });
+});
+
+describe('getDataZoomOption', () => {
+ it('returns an empty object when totalItems <= maxItemsPerPage', () => {
+ const totalItems = 10;
+ const maxItemsPerPage = 20;
+
+ expect(getDataZoomOption({ totalItems, maxItemsPerPage })).toEqual({});
+ });
+
+ describe('when totalItems > maxItemsPerPage', () => {
+ const totalItems = 30;
+ const maxItemsPerPage = 20;
+
+ it('properly computes the end interval for the default datazoom config', () => {
+ const expected = [
+ {
+ type: 'slider',
+ bottom: 10,
+ start: 0,
+ end: 67,
+ },
+ ];
+
+ expect(getDataZoomOption({ totalItems, maxItemsPerPage })).toEqual(expected);
+ });
+
+ it('properly computes the end interval for a custom datazoom config', () => {
+ const dataZoom = [
+ { type: 'slider', bottom: 0, start: 0 },
+ { type: 'inside', start: 0 },
+ ];
+ const expected = [
+ {
+ type: 'slider',
+ bottom: 0,
+ start: 0,
+ end: 67,
+ },
+ {
+ type: 'inside',
+ start: 0,
+ end: 67,
+ },
+ ];
+
+ expect(getDataZoomOption({ totalItems, maxItemsPerPage, dataZoom })).toEqual(expected);
+ });
+ });
+});
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 61c6a1dd167..870375318e3 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,5 +1,5 @@
-import { GlForm } from '@gitlab/ui';
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 ManageTwoFactorForm, {
diff --git a/spec/frontend/blob/file_template_mediator_spec.js b/spec/frontend/blob/file_template_mediator_spec.js
new file mode 100644
index 00000000000..44e12deb564
--- /dev/null
+++ b/spec/frontend/blob/file_template_mediator_spec.js
@@ -0,0 +1,53 @@
+import TemplateSelectorMediator from '~/blob/file_template_mediator';
+
+describe('Template Selector Mediator', () => {
+ let mediator;
+
+ describe('setFilename', () => {
+ let input;
+ const newFileName = 'foo';
+ const editor = jest.fn().mockImplementationOnce(() => ({
+ getValue: jest.fn().mockImplementation(() => {}),
+ }))();
+
+ beforeEach(() => {
+ setFixtures('<div class="file-editor"><input class="js-file-path-name-input" /></div>');
+ input = document.querySelector('.js-file-path-name-input');
+ mediator = new TemplateSelectorMediator({
+ editor,
+ currentAction: jest.fn(),
+ projectId: jest.fn(),
+ });
+ });
+
+ it('fills out the input field', () => {
+ expect(input.value).toBe('');
+ mediator.setFilename(newFileName);
+ expect(input.value).toBe(newFileName);
+ });
+
+ it.each`
+ name | newName | shouldDispatch
+ ${newFileName} | ${newFileName} | ${false}
+ ${newFileName} | ${''} | ${true}
+ ${newFileName} | ${undefined} | ${false}
+ ${''} | ${''} | ${false}
+ ${''} | ${newFileName} | ${true}
+ ${''} | ${undefined} | ${false}
+ `(
+ 'correctly reacts to the name change when current name is $name and newName is $newName',
+ ({ name, newName, shouldDispatch }) => {
+ input.value = name;
+ const eventHandler = jest.fn();
+ input.addEventListener('change', eventHandler);
+
+ mediator.setFilename(newName);
+ if (shouldDispatch) {
+ expect(eventHandler).toHaveBeenCalledTimes(1);
+ } else {
+ expect(eventHandler).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
+});
diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
new file mode 100644
index 00000000000..c35f2463f69
--- /dev/null
+++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
@@ -0,0 +1,59 @@
+import { GlButton } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
+import { createStore } from '~/boards/stores';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+Vue.use(Vuex);
+
+describe('BoardAddNewColumnTrigger', () => {
+ let wrapper;
+
+ const findBoardsCreateList = () => wrapper.findByTestId('boards-create-list');
+ const findTooltipText = () => getBinding(findBoardsCreateList().element, 'gl-tooltip');
+
+ const mountComponent = () => {
+ wrapper = mountExtended(BoardAddNewColumnTrigger, {
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ store: createStore(),
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when button is active', () => {
+ it('does not show the tooltip', () => {
+ const tooltip = findTooltipText();
+
+ expect(tooltip.value).toBe('');
+ });
+
+ it('renders an enabled button', () => {
+ const button = wrapper.find(GlButton);
+
+ expect(button.props('disabled')).toBe(false);
+ });
+ });
+
+ describe('when button is disabled', () => {
+ it('shows the tooltip', async () => {
+ wrapper.find(GlButton).vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const tooltip = findTooltipText();
+
+ expect(tooltip.value).toBe('The list creation wizard is already open');
+ });
+ });
+});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 62e0fa7a68a..0b90912a584 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -21,9 +21,10 @@ import {
getMoveData,
updateListPosition,
} from '~/boards/boards_util';
+import { gqlClient } from '~/boards/graphql';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
-import actions, { gqlClient } from '~/boards/stores/actions';
+import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
@@ -1331,20 +1332,54 @@ describe('addListItem', () => {
list: mockLists[0],
item: mockIssue,
position: 0,
+ inProgress: true,
};
- testAction(actions.addListItem, payload, {}, [
- {
- type: types.ADD_BOARD_ITEM_TO_LIST,
- payload: {
- listId: mockLists[0].id,
- itemId: mockIssue.id,
- atIndex: 0,
- inProgress: false,
+ testAction(
+ actions.addListItem,
+ payload,
+ {},
+ [
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: {
+ listId: mockLists[0].id,
+ itemId: mockIssue.id,
+ atIndex: 0,
+ inProgress: true,
+ },
},
- },
- { type: types.UPDATE_BOARD_ITEM, payload: mockIssue },
- ]);
+ { type: types.UPDATE_BOARD_ITEM, payload: mockIssue },
+ ],
+ [],
+ );
+ });
+
+ it('should commit ADD_BOARD_ITEM_TO_LIST and UPDATE_BOARD_ITEM mutations, dispatch setActiveId action when inProgress is false', () => {
+ const payload = {
+ list: mockLists[0],
+ item: mockIssue,
+ position: 0,
+ };
+
+ testAction(
+ actions.addListItem,
+ payload,
+ {},
+ [
+ {
+ type: types.ADD_BOARD_ITEM_TO_LIST,
+ payload: {
+ listId: mockLists[0].id,
+ itemId: mockIssue.id,
+ atIndex: 0,
+ inProgress: false,
+ },
+ },
+ { type: types.UPDATE_BOARD_ITEM, payload: mockIssue },
+ ],
+ [{ type: 'setActiveId', payload: { id: mockIssue.id, sidebarType: ISSUABLE } }],
+ );
});
});
@@ -1542,7 +1577,7 @@ describe('setActiveIssueLabels', () => {
projectPath: 'h/b',
};
- it('should assign labels on success', (done) => {
+ it('should assign labels on success, and sets loading state for labels', (done) => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
@@ -1559,6 +1594,14 @@ 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,
},
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
new file mode 100644
index 00000000000..fd04ff8b3e7
--- /dev/null
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -0,0 +1,195 @@
+import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlSprintf, GlTab } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+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';
+import { useFakeDate } from 'helpers/fake_date';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('ClusterAgentShow', () => {
+ let wrapper;
+ useFakeDate([2021, 2, 15]);
+
+ const propsData = {
+ agentName: 'cluster-agent',
+ projectPath: 'path/to/project',
+ };
+
+ const defaultClusterAgent = {
+ id: '1',
+ createdAt: '2021-02-13T00:00:00Z',
+ createdByUser: {
+ name: 'user-1',
+ },
+ name: 'token-1',
+ tokens: {
+ count: 1,
+ nodes: [],
+ pageInfo: null,
+ },
+ };
+
+ const createWrapper = ({ clusterAgent, queryResponse = null }) => {
+ const agentQueryResponse =
+ queryResponse || jest.fn().mockResolvedValue({ data: { project: { clusterAgent } } });
+ const apolloProvider = createMockApollo([[getAgentQuery, agentQueryResponse]]);
+
+ wrapper = shallowMount(ClusterAgentShow, {
+ localVue,
+ apolloProvider,
+ propsData,
+ stubs: { GlSprintf, TimeAgoTooltip, GlTab },
+ });
+ };
+
+ const createWrapperWithoutApollo = ({ clusterAgent, loading = false }) => {
+ const $apollo = { queries: { clusterAgent: { loading } } };
+
+ wrapper = shallowMount(ClusterAgentShow, {
+ propsData,
+ mocks: { $apollo, clusterAgent },
+ 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();
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default behaviour', () => {
+ beforeEach(() => {
+ return createWrapper({ clusterAgent: defaultClusterAgent });
+ });
+
+ it('displays the agent name', () => {
+ expect(wrapper.text()).toContain(propsData.agentName);
+ });
+
+ it('displays agent create information', () => {
+ expect(findCreatedText()).toMatchInterpolatedText('Created by user-1 2 days ago');
+ });
+
+ it('displays token count', () => {
+ expect(findTokenCount()).toMatchInterpolatedText(
+ `${ClusterAgentShow.i18n.tokens} ${defaultClusterAgent.tokens.count}`,
+ );
+ });
+
+ it('renders token table', () => {
+ expect(wrapper.find(TokenTable).exists()).toBe(true);
+ });
+
+ it('should not render pagination buttons when there are no additional pages', () => {
+ expect(findPaginationButtons().exists()).toBe(false);
+ });
+ });
+
+ describe('when create user is unknown', () => {
+ const missingUser = {
+ ...defaultClusterAgent,
+ createdByUser: null,
+ };
+
+ beforeEach(() => {
+ return createWrapper({ clusterAgent: missingUser });
+ });
+
+ it('displays agent create information with unknown user', () => {
+ expect(findCreatedText()).toMatchInterpolatedText('Created by Unknown user 2 days ago');
+ });
+ });
+
+ describe('when token count is missing', () => {
+ const missingTokens = {
+ ...defaultClusterAgent,
+ tokens: null,
+ };
+
+ beforeEach(() => {
+ return createWrapper({ clusterAgent: missingTokens });
+ });
+
+ it('displays token header with no count', () => {
+ expect(findTokenCount()).toMatchInterpolatedText(`${ClusterAgentShow.i18n.tokens}`);
+ });
+ });
+
+ describe('when the token list has additional pages', () => {
+ const pageInfo = {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'prev',
+ endCursor: 'next',
+ };
+
+ const tokenPagination = {
+ ...defaultClusterAgent,
+ tokens: {
+ ...defaultClusterAgent.tokens,
+ pageInfo,
+ },
+ };
+
+ beforeEach(() => {
+ return createWrapper({ clusterAgent: tokenPagination });
+ });
+
+ it('should render pagination buttons', () => {
+ expect(findPaginationButtons().exists()).toBe(true);
+ });
+
+ it('should pass pageInfo to the pagination component', () => {
+ expect(findPaginationButtons().props()).toMatchObject(pageInfo);
+ });
+ });
+
+ describe('when the agent query is loading', () => {
+ describe('when the clusterAgent is missing', () => {
+ beforeEach(() => {
+ return createWrapper({
+ clusterAgent: null,
+ queryResponse: jest.fn().mockReturnValue(new Promise(() => {})),
+ });
+ });
+
+ it('displays a loading icon and hides the token tab', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(wrapper.text()).not.toContain(ClusterAgentShow.i18n.tokens);
+ });
+ });
+
+ describe('when the clusterAgent is present', () => {
+ beforeEach(() => {
+ createWrapperWithoutApollo({ clusterAgent: defaultClusterAgent, loading: true });
+ });
+
+ it('displays a loading icon and token tab', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(wrapper.text()).toContain(ClusterAgentShow.i18n.tokens);
+ });
+ });
+ });
+
+ describe('when the agent query has errored', () => {
+ beforeEach(() => {
+ createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() });
+ return waitForPromises();
+ });
+
+ it('displays an alert message', () => {
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.text()).toContain(ClusterAgentShow.i18n.loadingError);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/agents/components/token_table_spec.js b/spec/frontend/clusters/agents/components/token_table_spec.js
new file mode 100644
index 00000000000..47ff944dd84
--- /dev/null
+++ b/spec/frontend/clusters/agents/components/token_table_spec.js
@@ -0,0 +1,135 @@
+import { GlEmptyState, GlLink, GlTooltip, GlTruncate } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import TokenTable from '~/clusters/agents/components/token_table.vue';
+import { useFakeDate } from 'helpers/fake_date';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+describe('ClusterAgentTokenTable', () => {
+ let wrapper;
+ useFakeDate([2021, 2, 15]);
+
+ const defaultTokens = [
+ {
+ id: '1',
+ createdAt: '2021-02-13T00:00:00Z',
+ description: 'Description of token 1',
+ createdByUser: {
+ name: 'user-1',
+ },
+ lastUsedAt: '2021-02-13T00:00:00Z',
+ name: 'token-1',
+ },
+ {
+ id: '2',
+ createdAt: '2021-02-10T00:00:00Z',
+ description: null,
+ createdByUser: null,
+ lastUsedAt: null,
+ name: 'token-2',
+ },
+ ];
+
+ const createComponent = (tokens) => {
+ wrapper = extendedWrapper(mount(TokenTable, { propsData: { tokens } }));
+ };
+
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findLink = () => wrapper.find(GlLink);
+
+ beforeEach(() => {
+ return createComponent(defaultTokens);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a learn more link', () => {
+ const learnMoreLink = findLink();
+
+ expect(learnMoreLink.exists()).toBe(true);
+ expect(learnMoreLink.text()).toBe(TokenTable.i18n.learnMore);
+ });
+
+ it.each`
+ name | lineNumber
+ ${'token-1'} | ${0}
+ ${'token-2'} | ${1}
+ `('displays token name "$name" for line "$lineNumber"', ({ name, lineNumber }) => {
+ const tokens = wrapper.findAll('[data-testid="agent-token-name"]');
+ const token = tokens.at(lineNumber);
+
+ expect(token.text()).toBe(name);
+ });
+
+ it.each`
+ lastContactText | lineNumber
+ ${'2 days ago'} | ${0}
+ ${'Never'} | ${1}
+ `(
+ 'displays last contact information "$lastContactText" for line "$lineNumber"',
+ ({ lastContactText, lineNumber }) => {
+ const tokens = wrapper.findAllByTestId('agent-token-used');
+ const token = tokens.at(lineNumber);
+
+ expect(token.text()).toBe(lastContactText);
+ },
+ );
+
+ it.each`
+ createdText | lineNumber
+ ${'2 days ago'} | ${0}
+ ${'5 days ago'} | ${1}
+ `(
+ 'displays created information "$createdText" for line "$lineNumber"',
+ ({ createdText, lineNumber }) => {
+ const tokens = wrapper.findAll('[data-testid="agent-token-created-time"]');
+ const token = tokens.at(lineNumber);
+
+ expect(token.text()).toBe(createdText);
+ },
+ );
+
+ it.each`
+ createdBy | lineNumber
+ ${'user-1'} | ${0}
+ ${'Unknown user'} | ${1}
+ `(
+ 'displays creator information "$createdBy" for line "$lineNumber"',
+ ({ createdBy, lineNumber }) => {
+ const tokens = wrapper.findAll('[data-testid="agent-token-created-user"]');
+ const token = tokens.at(lineNumber);
+
+ expect(token.text()).toBe(createdBy);
+ },
+ );
+
+ it.each`
+ description | truncatesText | hasTooltip | lineNumber
+ ${'Description of token 1'} | ${true} | ${true} | ${0}
+ ${''} | ${false} | ${false} | ${1}
+ `(
+ 'displays description information "$description" for line "$lineNumber"',
+ ({ description, truncatesText, hasTooltip, lineNumber }) => {
+ const tokens = wrapper.findAll('[data-testid="agent-token-description"]');
+ const token = tokens.at(lineNumber);
+
+ expect(token.text()).toContain(description);
+ expect(token.find(GlTruncate).exists()).toBe(truncatesText);
+ expect(token.find(GlTooltip).exists()).toBe(hasTooltip);
+ },
+ );
+
+ describe('when there are no tokens', () => {
+ beforeEach(() => {
+ return createComponent([]);
+ });
+
+ it('displays an empty state', () => {
+ const emptyState = findEmptyState();
+
+ expect(emptyState.exists()).toBe(true);
+ expect(emptyState.text()).toContain(TokenTable.i18n.noTokens);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index b34265b7234..42d81900911 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -33,7 +33,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
<span
class="sr-only"
>
- Toggle Dropdown
+ Toggle dropdown
</span>
</button>
<ul
@@ -46,21 +46,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
>
<!---->
- <div
- class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5"
- >
- <div
- class="gl-display-flex"
- >
- <!---->
- </div>
-
- <div
- class="gl-display-flex"
- >
- <!---->
- </div>
- </div>
+ <!---->
<div
class="gl-new-dropdown-contents"
diff --git a/spec/frontend/clusters_list/components/agent_empty_state_spec.js b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
new file mode 100644
index 00000000000..a548721588e
--- /dev/null
+++ b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
@@ -0,0 +1,77 @@
+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';
+
+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';
+
+describe('AgentEmptyStateComponent', () => {
+ let wrapper;
+
+ const propsData = {
+ hasConfigurations: false,
+ };
+ const provideData = {
+ emptyStateImage,
+ projectPath,
+ agentDocsUrl,
+ installDocsUrl,
+ getStartedDocsUrl,
+ integrationDocsUrl,
+ };
+
+ const findConfigurationsAlert = () => wrapper.findComponent(GlAlert);
+ const findAgentDocsLink = () => wrapper.findByTestId('agent-docs-link');
+ const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link');
+ const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button');
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(AgentEmptyState, {
+ propsData,
+ provide: provideData,
+ stubs: { GlEmptyState, GlSprintf },
+ });
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ it('renders correct href attributes for the links', () => {
+ expect(findAgentDocsLink().attributes('href')).toBe(agentDocsUrl);
+ expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
+ });
+
+ describe('when there are no agent configurations in repository', () => {
+ it('should render notification message box', () => {
+ expect(findConfigurationsAlert().exists()).toBe(true);
+ });
+
+ it('should disable integration button', () => {
+ expect(findIntegrationButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when there is a list of agent configurations', () => {
+ beforeEach(() => {
+ propsData.hasConfigurations = true;
+ wrapper = shallowMountExtended(AgentEmptyState, {
+ propsData,
+ provide: provideData,
+ });
+ });
+ it('should render content without notification message box', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findConfigurationsAlert().exists()).toBe(false);
+ expect(findIntegrationButton().attributes('disabled')).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
new file mode 100644
index 00000000000..e3b90584f29
--- /dev/null
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -0,0 +1,117 @@
+import { GlButton, 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';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+const connectedTimeNow = new Date();
+const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+
+const propsData = {
+ agents: [
+ {
+ name: 'agent-1',
+ configFolder: {
+ webPath: '/agent/full/path',
+ },
+ webPath: '/agent-1',
+ status: 'unused',
+ lastContact: null,
+ tokens: null,
+ },
+ {
+ name: 'agent-2',
+ webPath: '/agent-2',
+ status: 'active',
+ lastContact: connectedTimeNow.getTime(),
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeNow,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-3',
+ webPath: '/agent-3',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ ],
+};
+const provideData = { integrationDocsUrl: 'path/to/integrationDocs' };
+
+describe('AgentTable', () => {
+ let wrapper;
+
+ const findAgentLink = (at) => wrapper.findAllByTestId('cluster-agent-name-link').at(at);
+ const findStatusIcon = (at) => wrapper.findAllComponents(GlIcon).at(at);
+ const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at);
+ const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
+ const findConfiguration = (at) =>
+ wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
+
+ beforeEach(() => {
+ wrapper = mountExtended(AgentTable, { propsData, provide: provideData });
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ it('displays header button', () => {
+ expect(wrapper.find(GlButton).text()).toBe('Install a new GitLab Agent');
+ });
+
+ describe('agent table', () => {
+ it.each`
+ agentName | link | lineNumber
+ ${'agent-1'} | ${'/agent-1'} | ${0}
+ ${'agent-2'} | ${'/agent-2'} | ${1}
+ `('displays agent link', ({ agentName, link, lineNumber }) => {
+ expect(findAgentLink(lineNumber).text()).toBe(agentName);
+ expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
+ });
+
+ it.each`
+ status | iconName | lineNumber
+ ${'Never connected'} | ${'status-neutral'} | ${0}
+ ${'Connected'} | ${'status-success'} | ${1}
+ ${'Not connected'} | ${'severity-critical'} | ${2}
+ `('displays agent connection status', ({ status, iconName, lineNumber }) => {
+ expect(findStatusText(lineNumber).text()).toBe(status);
+ expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
+ });
+
+ it.each`
+ lastContact | lineNumber
+ ${'Never'} | ${0}
+ ${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
+ ${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
+ `('displays agent last contact time', ({ lastContact, lineNumber }) => {
+ expect(findLastContactText(lineNumber).text()).toBe(lastContact);
+ });
+
+ it.each`
+ agentPath | hasLink | lineNumber
+ ${'.gitlab/agents/agent-1'} | ${true} | ${0}
+ ${'.gitlab/agents/agent-2'} | ${false} | ${1}
+ `('displays config file path', ({ agentPath, hasLink, lineNumber }) => {
+ const findLink = findConfiguration(lineNumber).find(GlLink);
+
+ expect(findLink.exists()).toBe(hasLink);
+ expect(findConfiguration(lineNumber).text()).toBe(agentPath);
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
new file mode 100644
index 00000000000..54d5ae94172
--- /dev/null
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -0,0 +1,246 @@
+import { GlAlert, GlKeysetPagination, GlLoadingIcon } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
+import AgentTable from '~/clusters_list/components/agent_table.vue';
+import Agents from '~/clusters_list/components/agents.vue';
+import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('Agents', () => {
+ let wrapper;
+
+ const propsData = {
+ defaultBranchName: 'default',
+ };
+ const provideData = {
+ projectPath: 'path/to/project',
+ kasAddress: 'kas.example.com',
+ };
+
+ const createWrapper = ({ agents = [], pageInfo = null, trees = [] }) => {
+ const provide = provideData;
+ const apolloQueryResponse = {
+ data: {
+ project: {
+ clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] } },
+ repository: { tree: { trees: { nodes: trees, pageInfo } } },
+ },
+ },
+ };
+
+ const apolloProvider = createMockApollo([
+ [getAgentsQuery, jest.fn().mockResolvedValue(apolloQueryResponse, provide)],
+ ]);
+
+ wrapper = shallowMount(Agents, {
+ localVue,
+ apolloProvider,
+ propsData,
+ provide: provideData,
+ });
+
+ return wrapper.vm.$nextTick();
+ };
+
+ const findAgentTable = () => wrapper.find(AgentTable);
+ const findEmptyState = () => wrapper.find(AgentEmptyState);
+ const findPaginationButtons = () => wrapper.find(GlKeysetPagination);
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ describe('when there is a list of agents', () => {
+ let testDate = new Date();
+ const agents = [
+ {
+ id: '1',
+ name: 'agent-1',
+ webPath: '/agent-1',
+ tokens: null,
+ },
+ {
+ id: '2',
+ name: 'agent-2',
+ webPath: '/agent-2',
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: testDate,
+ },
+ ],
+ },
+ },
+ ];
+
+ const trees = [
+ {
+ name: 'agent-2',
+ path: '.gitlab/agents/agent-2',
+ webPath: '/project/path/.gitlab/agents/agent-2',
+ },
+ ];
+
+ const expectedAgentsList = [
+ {
+ id: '1',
+ name: 'agent-1',
+ webPath: '/agent-1',
+ configFolder: undefined,
+ status: 'unused',
+ lastContact: null,
+ tokens: null,
+ },
+ {
+ id: '2',
+ name: 'agent-2',
+ configFolder: {
+ name: 'agent-2',
+ path: '.gitlab/agents/agent-2',
+ webPath: '/project/path/.gitlab/agents/agent-2',
+ },
+ webPath: '/agent-2',
+ status: 'active',
+ lastContact: new Date(testDate).getTime(),
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: testDate,
+ },
+ ],
+ },
+ },
+ ];
+
+ beforeEach(() => {
+ return createWrapper({ agents, trees });
+ });
+
+ it('should render agent table', () => {
+ expect(findAgentTable().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('should pass agent and folder info to table component', () => {
+ expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
+ });
+
+ describe('when the agent has recently connected tokens', () => {
+ it('should set agent status to active', () => {
+ expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
+ });
+ });
+
+ describe('when the agent has tokens connected more then 8 minutes ago', () => {
+ const now = new Date();
+ testDate = new Date(now.getTime() - ACTIVE_CONNECTION_TIME);
+ it('should set agent status to inactive', () => {
+ expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
+ });
+ });
+
+ describe('when the agent has no connected tokens', () => {
+ testDate = null;
+ it('should set agent status to unused', () => {
+ expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
+ });
+ });
+
+ it('should not render pagination buttons when there are no additional pages', () => {
+ expect(findPaginationButtons().exists()).toBe(false);
+ });
+
+ describe('when the list has additional pages', () => {
+ const pageInfo = {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'prev',
+ endCursor: 'next',
+ };
+
+ beforeEach(() => {
+ return createWrapper({
+ agents,
+ pageInfo,
+ });
+ });
+
+ it('should render pagination buttons', () => {
+ expect(findPaginationButtons().exists()).toBe(true);
+ });
+
+ it('should pass pageInfo to the pagination component', () => {
+ expect(findPaginationButtons().props()).toMatchObject(pageInfo);
+ });
+ });
+ });
+
+ describe('when the agent list is empty', () => {
+ beforeEach(() => {
+ return createWrapper({ agents: [] });
+ });
+
+ it('should render empty state', () => {
+ expect(findAgentTable().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+
+ describe('when the agent configurations are present', () => {
+ const trees = [
+ {
+ name: 'agent-1',
+ path: '.gitlab/agents/agent-1',
+ webPath: '/project/path/.gitlab/agents/agent-1',
+ },
+ ];
+
+ beforeEach(() => {
+ return createWrapper({ agents: [], trees });
+ });
+
+ it('should pass the correct hasConfigurations boolean value to empty state component', () => {
+ expect(findEmptyState().props('hasConfigurations')).toEqual(true);
+ });
+ });
+
+ describe('when agents query has errored', () => {
+ beforeEach(() => {
+ return createWrapper({ agents: null });
+ });
+
+ it('displays an alert message', () => {
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
+ });
+ });
+
+ describe('when agents query is loading', () => {
+ const mocks = {
+ $apollo: {
+ queries: {
+ agents: {
+ loading: true,
+ },
+ },
+ },
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMount(Agents, { mocks, propsData, provide: provideData });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays a loading icon', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
new file mode 100644
index 00000000000..40c2c59e187
--- /dev/null
+++ b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
@@ -0,0 +1,129 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { createLocalVue, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
+import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
+import agentConfigurationsQuery from '~/clusters_list/graphql/queries/agent_configurations.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { agentConfigurationsResponse } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('AvailableAgentsDropdown', () => {
+ let wrapper;
+
+ const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN;
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findConfiguredAgentItem = () => findDropdownItems().at(0);
+
+ const createWrapper = ({ propsData = {}, isLoading = false }) => {
+ const provide = {
+ projectPath: 'path/to/project',
+ };
+
+ wrapper = (() => {
+ if (isLoading) {
+ const mocks = {
+ $apollo: {
+ queries: {
+ agents: {
+ loading: true,
+ },
+ },
+ },
+ };
+
+ return mount(AvailableAgentsDropdown, { mocks, provide, propsData });
+ }
+
+ const apolloProvider = createMockApollo([
+ [agentConfigurationsQuery, jest.fn().mockResolvedValue(agentConfigurationsResponse)],
+ ]);
+
+ return mount(AvailableAgentsDropdown, {
+ localVue,
+ apolloProvider,
+ provide,
+ propsData,
+ });
+ })();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('there are agents available', () => {
+ const propsData = {
+ isRegistering: false,
+ };
+
+ beforeEach(() => {
+ createWrapper({ propsData });
+ });
+
+ it('prompts to select an agent', () => {
+ expect(findDropdown().props('text')).toBe(i18n.selectAgent);
+ });
+
+ it('shows only agents that are not yet installed', () => {
+ expect(findDropdownItems()).toHaveLength(1);
+ expect(findConfiguredAgentItem().text()).toBe('configured-agent');
+ expect(findConfiguredAgentItem().props('isChecked')).toBe(false);
+ });
+
+ describe('click events', () => {
+ beforeEach(() => {
+ findConfiguredAgentItem().vm.$emit('click');
+ });
+
+ it('emits agentSelected with the name of the clicked agent', () => {
+ expect(wrapper.emitted('agentSelected')).toEqual([['configured-agent']]);
+ });
+
+ it('marks the clicked item as selected', () => {
+ expect(findDropdown().props('text')).toBe('configured-agent');
+ expect(findConfiguredAgentItem().props('isChecked')).toBe(true);
+ });
+ });
+ });
+
+ describe('registration in progress', () => {
+ const propsData = {
+ isRegistering: true,
+ };
+
+ beforeEach(() => {
+ createWrapper({ propsData });
+ });
+
+ it('updates the text in the dropdown', () => {
+ expect(findDropdown().props('text')).toBe(i18n.registeringAgent);
+ });
+
+ it('displays a loading icon', () => {
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+ });
+
+ describe('agents query is loading', () => {
+ const propsData = {
+ isRegistering: false,
+ };
+
+ beforeEach(() => {
+ createWrapper({ propsData, isLoading: true });
+ });
+
+ it('updates the text in the dropdown', () => {
+ expect(findDropdown().text()).toBe(i18n.selectAgent);
+ });
+
+ it('displays a loading icon', () => {
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
new file mode 100644
index 00000000000..98ca5e05b3f
--- /dev/null
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -0,0 +1,190 @@
+import { GlAlert, GlButton, GlFormInputGroup } from '@gitlab/ui';
+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 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';
+import waitForPromises from 'helpers/wait_for_promises';
+import CodeBlock from '~/vue_shared/components/code_block.vue';
+import {
+ createAgentResponse,
+ createAgentErrorResponse,
+ createAgentTokenResponse,
+ createAgentTokenErrorResponse,
+} from '../mocks/apollo';
+import ModalStub from '../stubs';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('InstallAgentModal', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const i18n = I18N_INSTALL_AGENT_MODAL;
+ const findModal = () => wrapper.findComponent(ModalStub);
+ const findAgentDropdown = () => findModal().findComponent(AvailableAgentsDropdown);
+ const findAlert = () => findModal().findComponent(GlAlert);
+ const findButtonByVariant = (variant) =>
+ findModal()
+ .findAll(GlButton)
+ .wrappers.find((button) => button.props('variant') === variant);
+ const findActionButton = () => findButtonByVariant('confirm');
+ const findCancelButton = () => findButtonByVariant('default');
+
+ const expectDisabledAttribute = (element, disabled) => {
+ if (disabled) {
+ expect(element.attributes('disabled')).toBe('true');
+ } else {
+ expect(element.attributes('disabled')).toBeUndefined();
+ }
+ };
+
+ const createWrapper = () => {
+ const provide = {
+ projectPath: 'path/to/project',
+ kasAddress: 'kas.example.com',
+ };
+
+ wrapper = shallowMount(InstallAgentModal, {
+ attachTo: document.body,
+ stubs: {
+ GlModal: ModalStub,
+ },
+ localVue,
+ apolloProvider,
+ provide,
+ });
+ };
+
+ const mockSelectedAgentResponse = () => {
+ createWrapper();
+
+ wrapper.vm.setAgentName('agent-name');
+ findActionButton().vm.$emit('click');
+
+ return waitForPromises();
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ apolloProvider = null;
+ });
+
+ describe('initial state', () => {
+ it('renders the dropdown for available agents', () => {
+ expect(findAgentDropdown().isVisible()).toBe(true);
+ expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
+ expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
+ expect(findModal().findComponent(GlAlert).exists()).toBe(false);
+ expect(findModal().findComponent(CodeBlock).exists()).toBe(false);
+ });
+
+ it('renders a cancel button', () => {
+ expect(findCancelButton().isVisible()).toBe(true);
+ expectDisabledAttribute(findCancelButton(), false);
+ });
+
+ it('renders a disabled next button', () => {
+ expect(findActionButton().isVisible()).toBe(true);
+ expect(findActionButton().text()).toBe(i18n.next);
+ expectDisabledAttribute(findActionButton(), true);
+ });
+ });
+
+ describe('an agent is selected', () => {
+ beforeEach(() => {
+ findAgentDropdown().vm.$emit('agentSelected');
+ });
+
+ it('enables the next button', () => {
+ expect(findActionButton().isVisible()).toBe(true);
+ expectDisabledAttribute(findActionButton(), false);
+ });
+ });
+
+ describe('registering an agent', () => {
+ const createAgentHandler = jest.fn().mockResolvedValue(createAgentResponse);
+ const createAgentTokenHandler = jest.fn().mockResolvedValue(createAgentTokenResponse);
+
+ beforeEach(() => {
+ apolloProvider = createMockApollo([
+ [createAgentMutation, createAgentHandler],
+ [createAgentTokenMutation, createAgentTokenHandler],
+ ]);
+
+ return mockSelectedAgentResponse(apolloProvider);
+ });
+
+ it('creates an agent and token', () => {
+ expect(createAgentHandler).toHaveBeenCalledWith({
+ input: { name: 'agent-name', projectPath: 'path/to/project' },
+ });
+
+ expect(createAgentTokenHandler).toHaveBeenCalledWith({
+ input: { clusterAgentId: 'agent-id', name: 'agent-name' },
+ });
+ });
+
+ it('renders a done button', () => {
+ expect(findActionButton().isVisible()).toBe(true);
+ expect(findActionButton().text()).toBe(i18n.done);
+ expectDisabledAttribute(findActionButton(), false);
+ });
+
+ it('shows agent instructions', () => {
+ const modalText = findModal().text();
+ expect(modalText).toContain(i18n.basicInstallTitle);
+ expect(modalText).toContain(i18n.basicInstallBody);
+
+ const token = findModal().findComponent(GlFormInputGroup);
+ expect(token.props('value')).toBe('mock-agent-token');
+
+ const alert = findModal().findComponent(GlAlert);
+ expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle);
+
+ const code = findModal().findComponent(CodeBlock).props('code');
+ expect(code).toContain('--agent-token=mock-agent-token');
+ expect(code).toContain('--kas-address=kas.example.com');
+ });
+
+ describe('error creating agent', () => {
+ beforeEach(() => {
+ apolloProvider = createMockApollo([
+ [createAgentMutation, jest.fn().mockResolvedValue(createAgentErrorResponse)],
+ ]);
+
+ return mockSelectedAgentResponse();
+ });
+
+ it('displays the error message', () => {
+ expect(findAlert().text()).toBe(createAgentErrorResponse.data.createClusterAgent.errors[0]);
+ });
+ });
+
+ describe('error creating token', () => {
+ beforeEach(() => {
+ apolloProvider = createMockApollo([
+ [createAgentMutation, jest.fn().mockResolvedValue(createAgentResponse)],
+ [createAgentTokenMutation, jest.fn().mockResolvedValue(createAgentTokenErrorResponse)],
+ ]);
+
+ return mockSelectedAgentResponse();
+ });
+
+ it('displays the error message', () => {
+ expect(findAlert().text()).toBe(
+ createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/mock_data.js b/spec/frontend/clusters_list/components/mock_data.js
new file mode 100644
index 00000000000..e388d791b89
--- /dev/null
+++ b/spec/frontend/clusters_list/components/mock_data.js
@@ -0,0 +1,12 @@
+export const agentConfigurationsResponse = {
+ data: {
+ project: {
+ agentConfigurations: {
+ nodes: [{ agentName: 'installed-agent' }, { agentName: 'configured-agent' }],
+ },
+ clusterAgents: {
+ nodes: [{ name: 'installed-agent' }],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js
new file mode 100644
index 00000000000..27b71a0d4b5
--- /dev/null
+++ b/spec/frontend/clusters_list/mocks/apollo.js
@@ -0,0 +1,45 @@
+export const createAgentResponse = {
+ data: {
+ createClusterAgent: {
+ clusterAgent: {
+ id: 'agent-id',
+ },
+ errors: [],
+ },
+ },
+};
+
+export const createAgentErrorResponse = {
+ data: {
+ createClusterAgent: {
+ clusterAgent: {
+ id: 'agent-id',
+ },
+ errors: ['could not create agent'],
+ },
+ },
+};
+
+export const createAgentTokenResponse = {
+ data: {
+ clusterAgentTokenCreate: {
+ token: {
+ id: 'token-id',
+ },
+ secret: 'mock-agent-token',
+ errors: [],
+ },
+ },
+};
+
+export const createAgentTokenErrorResponse = {
+ data: {
+ clusterAgentTokenCreate: {
+ token: {
+ id: 'token-id',
+ },
+ secret: 'mock-agent-token',
+ errors: ['could not create agent token'],
+ },
+ },
+};
diff --git a/spec/frontend/clusters_list/stubs.js b/spec/frontend/clusters_list/stubs.js
new file mode 100644
index 00000000000..5769d6190f6
--- /dev/null
+++ b/spec/frontend/clusters_list/stubs.js
@@ -0,0 +1,14 @@
+const ModalStub = {
+ name: 'glmodal-stub',
+ template: `
+ <div>
+ <slot></slot>
+ <slot name="modal-footer"></slot>
+ </div>
+ `,
+ methods: {
+ hide: jest.fn(),
+ },
+};
+
+export default ModalStub;
diff --git a/spec/frontend/comment_type_toggle_spec.js b/spec/frontend/comment_type_toggle_spec.js
deleted file mode 100644
index 06dbfac1803..00000000000
--- a/spec/frontend/comment_type_toggle_spec.js
+++ /dev/null
@@ -1,169 +0,0 @@
-import CommentTypeToggle from '~/comment_type_toggle';
-import DropLab from '~/droplab/drop_lab';
-import InputSetter from '~/droplab/plugins/input_setter';
-
-describe('CommentTypeToggle', () => {
- const testContext = {};
-
- describe('class constructor', () => {
- beforeEach(() => {
- testContext.dropdownTrigger = {};
- testContext.dropdownList = {};
- testContext.noteTypeInput = {};
- testContext.submitButton = {};
- testContext.closeButton = {};
-
- testContext.commentTypeToggle = new CommentTypeToggle({
- dropdownTrigger: testContext.dropdownTrigger,
- dropdownList: testContext.dropdownList,
- noteTypeInput: testContext.noteTypeInput,
- submitButton: testContext.submitButton,
- closeButton: testContext.closeButton,
- });
- });
-
- it('should set .dropdownTrigger', () => {
- expect(testContext.commentTypeToggle.dropdownTrigger).toBe(testContext.dropdownTrigger);
- });
-
- it('should set .dropdownList', () => {
- expect(testContext.commentTypeToggle.dropdownList).toBe(testContext.dropdownList);
- });
-
- it('should set .noteTypeInput', () => {
- expect(testContext.commentTypeToggle.noteTypeInput).toBe(testContext.noteTypeInput);
- });
-
- it('should set .submitButton', () => {
- expect(testContext.commentTypeToggle.submitButton).toBe(testContext.submitButton);
- });
-
- it('should set .closeButton', () => {
- expect(testContext.commentTypeToggle.closeButton).toBe(testContext.closeButton);
- });
-
- it('should set .reopenButton', () => {
- expect(testContext.commentTypeToggle.reopenButton).toBe(testContext.reopenButton);
- });
- });
-
- describe('initDroplab', () => {
- beforeEach(() => {
- testContext.commentTypeToggle = {
- dropdownTrigger: {},
- dropdownList: {},
- noteTypeInput: {},
- submitButton: {},
- closeButton: {},
- setConfig: () => {},
- };
- testContext.config = {};
-
- jest.spyOn(DropLab.prototype, 'init').mockImplementation();
- jest.spyOn(DropLab.prototype, 'constructor').mockImplementation();
-
- jest.spyOn(testContext.commentTypeToggle, 'setConfig').mockReturnValue(testContext.config);
-
- CommentTypeToggle.prototype.initDroplab.call(testContext.commentTypeToggle);
- });
-
- it('should instantiate a DropLab instance and set .droplab', () => {
- expect(testContext.commentTypeToggle.droplab instanceof DropLab).toBe(true);
- });
-
- it('should call .setConfig', () => {
- expect(testContext.commentTypeToggle.setConfig).toHaveBeenCalled();
- });
-
- it('should call DropLab.prototype.init', () => {
- expect(DropLab.prototype.init).toHaveBeenCalledWith(
- testContext.commentTypeToggle.dropdownTrigger,
- testContext.commentTypeToggle.dropdownList,
- [InputSetter],
- testContext.config,
- );
- });
- });
-
- describe('setConfig', () => {
- describe('if no .closeButton is provided', () => {
- beforeEach(() => {
- testContext.commentTypeToggle = {
- dropdownTrigger: {},
- dropdownList: {},
- noteTypeInput: {},
- submitButton: {},
- reopenButton: {},
- };
-
- testContext.setConfig = CommentTypeToggle.prototype.setConfig.call(
- testContext.commentTypeToggle,
- );
- });
-
- it('should not add .closeButton related InputSetter config', () => {
- expect(testContext.setConfig).toEqual({
- InputSetter: [
- {
- input: testContext.commentTypeToggle.noteTypeInput,
- valueAttribute: 'data-value',
- },
- {
- input: testContext.commentTypeToggle.submitButton,
- valueAttribute: 'data-submit-text',
- },
- {
- input: testContext.commentTypeToggle.reopenButton,
- valueAttribute: 'data-reopen-text',
- },
- {
- input: testContext.commentTypeToggle.reopenButton,
- valueAttribute: 'data-reopen-text',
- inputAttribute: 'data-alternative-text',
- },
- ],
- });
- });
- });
-
- describe('if no .reopenButton is provided', () => {
- beforeEach(() => {
- testContext.commentTypeToggle = {
- dropdownTrigger: {},
- dropdownList: {},
- noteTypeInput: {},
- submitButton: {},
- closeButton: {},
- };
-
- testContext.setConfig = CommentTypeToggle.prototype.setConfig.call(
- testContext.commentTypeToggle,
- );
- });
-
- it('should not add .reopenButton related InputSetter config', () => {
- expect(testContext.setConfig).toEqual({
- InputSetter: [
- {
- input: testContext.commentTypeToggle.noteTypeInput,
- valueAttribute: 'data-value',
- },
- {
- input: testContext.commentTypeToggle.submitButton,
- valueAttribute: 'data-submit-text',
- },
- {
- input: testContext.commentTypeToggle.closeButton,
- valueAttribute: 'data-close-text',
- },
- {
- input: testContext.commentTypeToggle.closeButton,
- valueAttribute: 'data-close-text',
- inputAttribute: 'data-alternative-text',
- },
- ],
- });
- });
- });
- });
-});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 8082b8524e7..3a549e66eb7 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
-import { getJSONFixture } from 'helpers/fixtures';
+import fixture from 'test_fixtures/pipelines/pipelines.json';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
@@ -20,7 +20,7 @@ jest.mock('~/projects/tree/services/commit_pipeline_service', () =>
describe('Commit pipeline status component', () => {
let wrapper;
- const { pipelines } = getJSONFixture('pipelines/pipelines.json');
+ const { pipelines } = fixture;
const { status: mockCiStatus } = pipelines[0].details;
const defaultProps = {
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 1defb3d586c..17f7be9d1d7 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -1,6 +1,7 @@
import { GlEmptyState, GlLoadingIcon, GlModal, GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
@@ -8,7 +9,6 @@ import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import axios from '~/lib/utils/axios_utils';
describe('Pipelines table in Commits and Merge requests', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
let wrapper;
let pipeline;
let mock;
@@ -37,7 +37,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- const { pipelines } = getJSONFixture(jsonFixtureName);
+ const { pipelines } = fixture;
pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
});
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
index 8f5516545eb..178c7d749c8 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -11,14 +11,7 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
<ul role=\\"menu\\" tabindex=\\"-1\\" class=\\"dropdown-menu\\">
<div class=\\"gl-new-dropdown-inner\\">
<!---->
- <div class=\\"gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5\\">
- <div class=\\"gl-display-flex\\">
- <!---->
- </div>
- <div class=\\"gl-display-flex\\">
- <!---->
- </div>
- </div>
+ <!---->
<div class=\\"gl-new-dropdown-contents\\">
<!---->
<li role=\\"presentation\\" class=\\"gl-px-3!\\">
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index a5df3d73289..ec58877470c 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -31,6 +31,7 @@ describe('content_editor/components/top_toolbar', () => {
${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
+ ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }}
${'horizontal-rule'} | ${{ contentType: 'horizontalRule', iconName: 'dash', label: 'Add a horizontal rule', editorCommand: 'setHorizontalRule' }}
${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
${'text-styles'} | ${{}}
diff --git a/spec/frontend/content_editor/components/wrappers/details_spec.js b/spec/frontend/content_editor/components/wrappers/details_spec.js
new file mode 100644
index 00000000000..d746b9fa2f1
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/details_spec.js
@@ -0,0 +1,40 @@
+import { NodeViewContent } from '@tiptap/vue-2';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DetailsWrapper from '~/content_editor/components/wrappers/details.vue';
+
+describe('content/components/wrappers/details', () => {
+ let wrapper;
+
+ const createWrapper = async () => {
+ wrapper = shallowMountExtended(DetailsWrapper, {
+ propsData: {
+ node: {},
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a node-view-content as a ul element', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(NodeViewContent).props().as).toBe('ul');
+ });
+
+ it('is "open" by default', () => {
+ createWrapper();
+
+ expect(wrapper.findByTestId('details-toggle-icon').classes()).toContain('is-open');
+ expect(wrapper.findComponent(NodeViewContent).classes()).toContain('is-open');
+ });
+
+ it('closes the details block on clicking the details toggle icon', async () => {
+ createWrapper();
+
+ await wrapper.findByTestId('details-toggle-icon').trigger('click');
+ expect(wrapper.findByTestId('details-toggle-icon').classes()).not.toContain('is-open');
+ expect(wrapper.findComponent(NodeViewContent).classes()).not.toContain('is-open');
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
new file mode 100644
index 00000000000..de8f8efd260
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/frontmatter_spec.js
@@ -0,0 +1,43 @@
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
+import { shallowMount } from '@vue/test-utils';
+import FrontmatterWrapper from '~/content_editor/components/wrappers/frontmatter.vue';
+
+describe('content/components/wrappers/frontmatter', () => {
+ let wrapper;
+
+ const createWrapper = async (nodeAttrs = { language: 'yaml' }) => {
+ wrapper = shallowMount(FrontmatterWrapper, {
+ propsData: {
+ node: {
+ attrs: nodeAttrs,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a node-view-wrapper as a pre element', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('pre');
+ expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
+ });
+
+ it('renders a node-view-content as a code element', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(NodeViewContent).props().as).toBe('code');
+ });
+
+ it('renders label indicating that code block is frontmatter', () => {
+ createWrapper();
+
+ const label = wrapper.find('[data-testid="frontmatter-label"]');
+
+ expect(label.text()).toEqual('frontmatter:yaml');
+ expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/color_chip_spec.js b/spec/frontend/content_editor/extensions/color_chip_spec.js
new file mode 100644
index 00000000000..4bb6f344ab4
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/color_chip_spec.js
@@ -0,0 +1,33 @@
+import ColorChip, { colorDecoratorPlugin } from '~/content_editor/extensions/color_chip';
+import Code from '~/content_editor/extensions/code';
+import { createTestEditor } from '../test_utils';
+
+describe('content_editor/extensions/color_chip', () => {
+ let tiptapEditor;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [ColorChip, Code] });
+ });
+
+ describe.each`
+ colorExpression | decorated
+ ${'#F00'} | ${true}
+ ${'rgba(0,0,0,0)'} | ${true}
+ ${'hsl(540,70%,50%)'} | ${true}
+ ${'F00'} | ${false}
+ ${'F00'} | ${false}
+ ${'gba(0,0,0,0)'} | ${false}
+ ${'hls(540,70%,50%)'} | ${false}
+ ${'red'} | ${false}
+ `(
+ 'when a code span with $colorExpression color expression is found',
+ ({ colorExpression, decorated }) => {
+ it(`${decorated ? 'adds' : 'does not add'} a color chip decorator`, () => {
+ tiptapEditor.commands.setContent(`<p><code>${colorExpression}</code></p>`);
+ const pluginState = colorDecoratorPlugin.getState(tiptapEditor.state);
+
+ expect(pluginState.children).toHaveLength(decorated ? 3 : 0);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/content_editor/extensions/details_content_spec.js b/spec/frontend/content_editor/extensions/details_content_spec.js
new file mode 100644
index 00000000000..575f3bf65e4
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/details_content_spec.js
@@ -0,0 +1,76 @@
+import Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/details_content', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let details;
+ let detailsContent;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Details, DetailsContent] });
+
+ ({
+ builders: { doc, p, details, detailsContent },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ details: { nodeType: Details.name },
+ detailsContent: { nodeType: DetailsContent.name },
+ },
+ }));
+ });
+
+ describe('shortcut: Enter', () => {
+ it('splits a details content into two items', () => {
+ const initialDoc = doc(
+ details(
+ detailsContent(p('Summary')),
+ detailsContent(p('Text content')),
+ detailsContent(p('Text content')),
+ ),
+ );
+ const expectedDoc = doc(
+ details(
+ detailsContent(p('Summary')),
+ detailsContent(p('')),
+ detailsContent(p('Text content')),
+ detailsContent(p('Text content')),
+ ),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.commands.setTextSelection(10);
+ tiptapEditor.commands.keyboardShortcut('Enter');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('shortcut: Shift-Tab', () => {
+ it('lifts a details content and creates two separate details items', () => {
+ const initialDoc = doc(
+ details(
+ detailsContent(p('Summary')),
+ detailsContent(p('Text content')),
+ detailsContent(p('Text content')),
+ ),
+ );
+ const expectedDoc = doc(
+ details(detailsContent(p('Summary'))),
+ p('Text content'),
+ details(detailsContent(p('Text content'))),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.commands.setTextSelection(20);
+ tiptapEditor.commands.keyboardShortcut('Shift-Tab');
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/details_spec.js b/spec/frontend/content_editor/extensions/details_spec.js
new file mode 100644
index 00000000000..cd59943982f
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/details_spec.js
@@ -0,0 +1,92 @@
+import Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/details', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let details;
+ let detailsContent;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Details, DetailsContent] });
+
+ ({
+ builders: { doc, p, details, detailsContent },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ details: { nodeType: Details.name },
+ detailsContent: { nodeType: DetailsContent.name },
+ },
+ }));
+ });
+
+ describe('setDetails command', () => {
+ describe('when current block is a paragraph', () => {
+ it('converts current paragraph into a details block', () => {
+ const initialDoc = doc(p('Text content'));
+ const expectedDoc = doc(details(detailsContent(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when current block is a details block', () => {
+ it('maintains the same document structure', () => {
+ const initialDoc = doc(details(detailsContent(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.setDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(initialDoc.toJSON());
+ });
+ });
+ });
+
+ describe('toggleDetails command', () => {
+ describe('when current block is a paragraph', () => {
+ it('converts current paragraph into a details block', () => {
+ const initialDoc = doc(p('Text content'));
+ const expectedDoc = doc(details(detailsContent(p('Text content'))));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.toggleDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when current block is a details block', () => {
+ it('convert details block into a paragraph', () => {
+ const initialDoc = doc(details(detailsContent(p('Text content'))));
+ const expectedDoc = doc(p('Text content'));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.toggleDetails();
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+ });
+
+ it.each`
+ input | insertedNode
+ ${'<details>'} | ${(...args) => details(detailsContent(p(...args)))}
+ ${'<details'} | ${(...args) => p(...args)}
+ ${'details>'} | ${(...args) => p(...args)}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const { view } = tiptapEditor;
+ const { selection } = view.state;
+ const expectedDoc = doc(insertedNode());
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, 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
new file mode 100644
index 00000000000..82eb85477de
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/math_inline_spec.js
@@ -0,0 +1,42 @@
+import MathInline from '~/content_editor/extensions/math_inline';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/math_inline', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let mathInline;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [MathInline] });
+
+ ({
+ builders: { doc, p, mathInline },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ details: { markType: MathInline.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'$`a^2`$'} | ${() => p(mathInline('a^2'))}
+ ${'$`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));
+
+ 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
new file mode 100644
index 00000000000..83818899c17
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/table_of_contents_spec.js
@@ -0,0 +1,35 @@
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/emoji', () => {
+ let tiptapEditor;
+ let builders;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [TableOfContents] });
+ ({ builders } = createDocBuilder({
+ tiptapEditor,
+ names: { tableOfContents: { nodeType: TableOfContents.name } },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'[[_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]());
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input));
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/markdown_processing_examples.js b/spec/frontend/content_editor/markdown_processing_examples.js
index b3aabfeb145..da895970289 100644
--- a/spec/frontend/content_editor/markdown_processing_examples.js
+++ b/spec/frontend/content_editor/markdown_processing_examples.js
@@ -1,11 +1,13 @@
import fs from 'fs';
import path from 'path';
import jsYaml from 'js-yaml';
+// eslint-disable-next-line import/no-deprecated
import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => {
const fixturePathPrefix = `api/markdown/${testName}.json`;
+ // eslint-disable-next-line import/no-deprecated
const fixture = getJSONFixture(fixturePathPrefix);
return fixture.body || fixture.html;
};
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 6f2c908c289..33056ab9e4a 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -5,6 +5,8 @@ import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
+import Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
import Division from '~/content_editor/extensions/division';
import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
@@ -45,6 +47,8 @@ const tiptapEditor = createTestEditor({
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
+ Details,
+ DetailsContent,
Division,
Emoji,
Figure,
@@ -78,6 +82,8 @@ const {
bulletList,
code,
codeBlock,
+ details,
+ detailsContent,
division,
descriptionItem,
descriptionList,
@@ -110,6 +116,8 @@ const {
bulletList: { nodeType: BulletList.name },
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
+ details: { nodeType: Details.name },
+ detailsContent: { nodeType: DetailsContent.name },
division: { nodeType: Division.name },
descriptionItem: { nodeType: DescriptionItem.name },
descriptionList: { nodeType: DescriptionList.name },
@@ -588,6 +596,105 @@ A giant _owl-like_ creature.
);
});
+ it('correctly renders a simple details/summary', () => {
+ expect(
+ serialize(
+ details(
+ detailsContent(paragraph('this is the summary')),
+ detailsContent(paragraph('this content will be hidden')),
+ ),
+ ),
+ ).toBe(
+ `
+<details>
+<summary>this is the summary</summary>
+this content will be hidden
+</details>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders details/summary with styled content', () => {
+ expect(
+ serialize(
+ details(
+ detailsContent(paragraph('this is the ', bold('summary'))),
+ detailsContent(
+ codeBlock(
+ { language: 'javascript' },
+ 'var a = 2;\nvar b = 3;\nvar c = a + d;\n\nconsole.log(c);',
+ ),
+ ),
+ detailsContent(paragraph('this content will be ', italic('hidden'))),
+ ),
+ details(detailsContent(paragraph('summary 2')), detailsContent(paragraph('content 2'))),
+ ),
+ ).toBe(
+ `
+<details>
+<summary>
+
+this is the **summary**
+
+</summary>
+
+\`\`\`javascript
+var a = 2;
+var b = 3;
+var c = a + d;
+
+console.log(c);
+\`\`\`
+
+this content will be _hidden_
+
+</details>
+<details>
+<summary>summary 2</summary>
+content 2
+</details>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders nested details', () => {
+ expect(
+ serialize(
+ details(
+ detailsContent(paragraph('dream level 1')),
+ detailsContent(
+ details(
+ detailsContent(paragraph('dream level 2')),
+ detailsContent(
+ details(
+ detailsContent(paragraph('dream level 3')),
+ detailsContent(paragraph(italic('inception'))),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+<details>
+<summary>dream level 1</summary>
+
+<details>
+<summary>dream level 2</summary>
+
+<details>
+<summary>dream level 3</summary>
+
+_inception_
+
+</details>
+</details>
+</details>
+ `.trim(),
+ );
+ });
+
it('correctly renders div', () => {
expect(
serialize(
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
index 5d3361bfa35..9a9415cc12a 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -19,6 +19,7 @@ import {
createdAfter,
currentGroup,
stageCounts,
+ initialPaginationState as pagination,
} from './mock_data';
const selectedStageEvents = issueEvents.events;
@@ -81,6 +82,7 @@ const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics);
const findStageTable = () => wrapper.findComponent(StageTable);
const findStageEvents = () => findStageTable().props('stageEvents');
const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title');
+const findPagination = () => wrapper.findByTestId('vsa-stage-pagination');
const hasMetricsRequests = (reqs) => {
const foundReqs = findOverviewMetrics().props('requests');
@@ -90,7 +92,7 @@ const hasMetricsRequests = (reqs) => {
describe('Value stream analytics component', () => {
beforeEach(() => {
- wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents } });
+ wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents, pagination } });
});
afterEach(() => {
@@ -153,6 +155,10 @@ describe('Value stream analytics component', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
+ it('renders pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
describe('with `cycleAnalyticsForGroups=true` license', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } });
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index d9659d5d4c3..1882457960a 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -1,6 +1,14 @@
+/* eslint-disable import/no-deprecated */
+
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
-import { DEFAULT_VALUE_STREAM, DEFAULT_DAYS_IN_PAST } from '~/cycle_analytics/constants';
+import {
+ DEFAULT_VALUE_STREAM,
+ DEFAULT_DAYS_IN_PAST,
+ PAGINATION_TYPE,
+ PAGINATION_SORT_DIRECTION_DESC,
+ PAGINATION_SORT_FIELD_END_EVENT,
+} from '~/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
@@ -13,9 +21,10 @@ export const getStageByTitle = (stages, title) =>
stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {};
const fixtureEndpoints = {
- customizableCycleAnalyticsStagesAndEvents: 'projects/analytics/value_stream_analytics/stages',
- stageEvents: (stage) => `projects/analytics/value_stream_analytics/events/${stage}`,
- metricsData: 'projects/analytics/value_stream_analytics/summary',
+ 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);
@@ -256,3 +265,22 @@ export const rawValueStreamStages = customizableStagesAndEvents.stages;
export const valueStreamStages = rawValueStreamStages.map((s) =>
convertObjectPropsToCamelCase(s, { deep: true }),
);
+
+export const initialPaginationQuery = {
+ page: 15,
+ sort: PAGINATION_SORT_FIELD_END_EVENT,
+ direction: PAGINATION_SORT_DIRECTION_DESC,
+};
+
+export const initialPaginationState = {
+ ...initialPaginationQuery,
+ page: null,
+ hasNextPage: false,
+};
+
+export const basePaginationResult = {
+ pagination: PAGINATION_TYPE,
+ sort: PAGINATION_SORT_FIELD_END_EVENT,
+ direction: PAGINATION_SORT_DIRECTION_DESC,
+ page: null,
+};
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index 97b5bd03e18..993e6b6b73a 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -11,6 +11,8 @@ import {
currentGroup,
createdAfter,
createdBefore,
+ initialPaginationState,
+ reviewEvents,
} from '../mock_data';
const { id: groupId, path: groupPath } = currentGroup;
@@ -31,7 +33,13 @@ const mockSetDateActionCommit = {
type: 'SET_DATE_RANGE',
};
-const defaultState = { ...getters, selectedValueStream, createdAfter, createdBefore };
+const defaultState = {
+ ...getters,
+ selectedValueStream,
+ createdAfter,
+ createdBefore,
+ pagination: initialPaginationState,
+};
describe('Project Value Stream Analytics actions', () => {
let state;
@@ -112,6 +120,21 @@ describe('Project Value Stream Analytics actions', () => {
});
});
+ describe('updateStageTablePagination', () => {
+ beforeEach(() => {
+ state = { ...state, selectedStage };
+ });
+
+ it(`will dispatch the "fetchStageData" action and commit the 'SET_PAGINATION' mutation`, () => {
+ return testAction({
+ action: actions.updateStageTablePagination,
+ state,
+ expectedMutations: [{ type: 'SET_PAGINATION' }],
+ expectedActions: [{ type: 'fetchStageData', payload: selectedStage.id }],
+ });
+ });
+ });
+
describe('fetchCycleAnalyticsData', () => {
beforeEach(() => {
state = { ...defaultState, endpoints: mockEndpoints };
@@ -154,6 +177,10 @@ describe('Project Value Stream Analytics actions', () => {
describe('fetchStageData', () => {
const mockStagePath = /value_streams\/\w+\/stages\/\w+\/records/;
+ const headers = {
+ 'X-Next-Page': 2,
+ 'X-Page': 1,
+ };
beforeEach(() => {
state = {
@@ -162,7 +189,7 @@ describe('Project Value Stream Analytics actions', () => {
selectedStage,
};
mock = new MockAdapter(axios);
- mock.onGet(mockStagePath).reply(httpStatusCodes.OK);
+ mock.onGet(mockStagePath).reply(httpStatusCodes.OK, reviewEvents, headers);
});
it(`commits the 'RECEIVE_STAGE_DATA_SUCCESS' mutation`, () =>
@@ -170,7 +197,11 @@ describe('Project Value Stream Analytics actions', () => {
action: actions.fetchStageData,
state,
payload: {},
- expectedMutations: [{ type: 'REQUEST_STAGE_DATA' }, { type: 'RECEIVE_STAGE_DATA_SUCCESS' }],
+ expectedMutations: [
+ { type: 'REQUEST_STAGE_DATA' },
+ { type: 'RECEIVE_STAGE_DATA_SUCCESS', payload: reviewEvents },
+ { type: 'SET_PAGINATION', payload: { hasNextPage: true, page: 1 } },
+ ],
expectedActions: [],
}));
diff --git a/spec/frontend/cycle_analytics/store/getters_spec.js b/spec/frontend/cycle_analytics/store/getters_spec.js
index c47a30a5f79..c9208045a68 100644
--- a/spec/frontend/cycle_analytics/store/getters_spec.js
+++ b/spec/frontend/cycle_analytics/store/getters_spec.js
@@ -1,17 +1,42 @@
import * as getters from '~/cycle_analytics/store/getters';
+
import {
allowedStages,
stageMedians,
transformedProjectStagePathData,
selectedStage,
stageCounts,
+ basePaginationResult,
+ initialPaginationState,
} from '../mock_data';
describe('Value stream analytics getters', () => {
+ let state = {};
+
describe('pathNavigationData', () => {
it('returns the transformed data', () => {
- const state = { stages: allowedStages, medians: stageMedians, selectedStage, stageCounts };
+ state = { stages: allowedStages, medians: stageMedians, selectedStage, stageCounts };
expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData);
});
});
+
+ describe('paginationParams', () => {
+ beforeEach(() => {
+ state = { pagination: initialPaginationState };
+ });
+
+ it('returns the `pagination` type', () => {
+ expect(getters.paginationParams(state)).toEqual(basePaginationResult);
+ });
+
+ it('returns the `sort` type', () => {
+ expect(getters.paginationParams(state)).toEqual(basePaginationResult);
+ });
+
+ it('with page=10, sets the `page` property', () => {
+ const page = 10;
+ state = { pagination: { ...initialPaginationState, page } };
+ expect(getters.paginationParams(state)).toEqual({ ...basePaginationResult, page });
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 628e2a4e7ae..4860225c995 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -2,6 +2,10 @@ import { useFakeDate } from 'helpers/fake_date';
import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations';
import {
+ PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_DIRECTION_DESC,
+} from '~/cycle_analytics/constants';
+import {
selectedStage,
rawIssueEvents,
issueEvents,
@@ -12,6 +16,7 @@ import {
formattedStageMedians,
rawStageCounts,
stageCounts,
+ initialPaginationState as pagination,
} from '../mock_data';
let state;
@@ -25,7 +30,7 @@ describe('Project Value Stream Analytics mutations', () => {
useFakeDate(2020, 6, 18);
beforeEach(() => {
- state = {};
+ state = { pagination };
});
afterEach(() => {
@@ -88,16 +93,18 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
- mutation | payload | stateKey | value
- ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
- ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
- ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
- ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
- ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
- ${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}
- ${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts}
+ mutation | payload | stateKey | value
+ ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
+ ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
+ ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
+ ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
+ ${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.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}
+ ${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index 69fed879fd8..74d64cd8d71 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,7 +1,6 @@
import { useFakeDate } from 'helpers/fake_date';
import {
transformStagesForPathNavigation,
- timeSummaryForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
@@ -47,21 +46,6 @@ describe('Value stream analytics utils', () => {
});
});
- describe('timeSummaryForPathNavigation', () => {
- it.each`
- unit | value | result
- ${'months'} | ${1.5} | ${'1.5M'}
- ${'weeks'} | ${1.25} | ${'1.5w'}
- ${'days'} | ${2} | ${'2d'}
- ${'hours'} | ${10} | ${'10h'}
- ${'minutes'} | ${20} | ${'20m'}
- ${'seconds'} | ${10} | ${'<1m'}
- ${'seconds'} | ${0} | ${'-'}
- `('will format $value $unit to $result', ({ unit, value, result }) => {
- expect(timeSummaryForPathNavigation({ [unit]: value })).toBe(result);
- });
- });
-
describe('medianTimeToParsedSeconds', () => {
it.each`
value | result
diff --git a/spec/frontend/deploy_freeze/helpers.js b/spec/frontend/deploy_freeze/helpers.js
index 598f14d45f6..43e66183ab5 100644
--- a/spec/frontend/deploy_freeze/helpers.js
+++ b/spec/frontend/deploy_freeze/helpers.js
@@ -1,7 +1,8 @@
+import freezePeriodsFixture from 'test_fixtures/api/freeze-periods/freeze_periods.json';
+import timezoneDataFixture from 'test_fixtures/timezones/short.json';
import { secondsToHours } from '~/lib/utils/datetime_utility';
-export const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json');
-export const timezoneDataFixture = getJSONFixture('/timezones/short.json');
+export { freezePeriodsFixture, timezoneDataFixture };
export const findTzByName = (identifier = '') =>
timezoneDataFixture.find(({ name }) => name.toLowerCase() === identifier.toLowerCase());
diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js
index 307a0b6d8b0..6ac68061518 100644
--- a/spec/frontend/deploy_keys/components/action_btn_spec.js
+++ b/spec/frontend/deploy_keys/components/action_btn_spec.js
@@ -1,10 +1,10 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import data from 'test_fixtures/deploy_keys/keys.json';
import actionBtn from '~/deploy_keys/components/action_btn.vue';
import eventHub from '~/deploy_keys/eventhub';
describe('Deploy keys action btn', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
const deployKey = data.enabled_keys[0];
let wrapper;
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index a72b2b00776..598b7a0f173 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import data from 'test_fixtures/deploy_keys/keys.json';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import deployKeysApp from '~/deploy_keys/components/app.vue';
@@ -10,7 +11,6 @@ import axios from '~/lib/utils/axios_utils';
const TEST_ENDPOINT = `${TEST_HOST}/dummy/`;
describe('Deploy keys app component', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
let wrapper;
let mock;
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 5420f9a01f9..511b9d6ef55 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import data from 'test_fixtures/deploy_keys/keys.json';
import key from '~/deploy_keys/components/key.vue';
import DeployKeysStore from '~/deploy_keys/store';
import { getTimeago } from '~/lib/utils/datetime_utility';
@@ -7,8 +8,6 @@ describe('Deploy keys key', () => {
let wrapper;
let store;
- const data = getJSONFixture('deploy_keys/keys.json');
-
const findTextAndTrim = (selector) => wrapper.find(selector).text().trim();
const createComponent = (propsData) => {
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
index d6419356166..f3b907e5450 100644
--- a/spec/frontend/deploy_keys/components/keys_panel_spec.js
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
+import data from 'test_fixtures/deploy_keys/keys.json';
import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
import DeployKeysStore from '~/deploy_keys/store';
describe('Deploy keys panel', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
let wrapper;
const findTableRowHeader = () => wrapper.find('.table-row-header');
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 4a6dee31cd5..7e4c6e131b4 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -3,6 +3,8 @@
import $ from 'jquery';
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', () => ({
@@ -66,6 +68,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');
});
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index 67e4a82787c..2b706d21f51 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -4,13 +4,13 @@ exports[`Design management design version dropdown component renders design vers
<gl-dropdown-stub
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
issueiid=""
projectpath=""
- showhighlighteditemstitle="true"
size="small"
text="Showing latest version"
variant="default"
@@ -85,13 +85,13 @@ exports[`Design management design version dropdown component renders design vers
<gl-dropdown-stub
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
issueiid=""
projectpath=""
- showhighlighteditemstitle="true"
size="small"
text="Showing latest version"
variant="default"
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index 7327cf00abd..fa6a666bb37 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -26,11 +26,11 @@ describe('Design Management cache update', () => {
describe('error handling', () => {
it.each`
- fnName | subject | errorMessage | extraArgs
- ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
- ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
- ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
- ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterRepositionImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
+ fnName | subject | errorMessage | extraArgs
+ ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError()} | ${[[design]]}
+ ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
+ ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
+ ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterRepositionImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
`('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => {
expect(createFlash).not.toHaveBeenCalled();
expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow();
diff --git a/spec/frontend/design_management/utils/error_messages_spec.js b/spec/frontend/design_management/utils/error_messages_spec.js
index b80dcd9abde..4994f4f6fd0 100644
--- a/spec/frontend/design_management/utils/error_messages_spec.js
+++ b/spec/frontend/design_management/utils/error_messages_spec.js
@@ -10,20 +10,21 @@ const mockFilenames = (n) =>
describe('Error message', () => {
describe('designDeletionError', () => {
- const singularMsg = 'Could not archive a design. Please try again.';
- const pluralMsg = 'Could not archive designs. Please try again.';
+ const singularMsg = 'Failed to archive a design. Please try again.';
+ const pluralMsg = 'Failed to archive designs. Please try again.';
- describe('when [singular=true]', () => {
- it.each([[undefined], [true]])('uses singular grammar', (singularOption) => {
- expect(designDeletionError({ singular: singularOption })).toEqual(singularMsg);
- });
- });
-
- describe('when [singular=false]', () => {
- it('uses plural grammar', () => {
- expect(designDeletionError({ singular: false })).toEqual(pluralMsg);
- });
- });
+ it.each`
+ designsLength | expectedText
+ ${undefined} | ${singularMsg}
+ ${0} | ${pluralMsg}
+ ${1} | ${singularMsg}
+ ${2} | ${pluralMsg}
+ `(
+ 'returns "$expectedText" when designsLength is $designsLength',
+ ({ designsLength, expectedText }) => {
+ expect(designDeletionError(designsLength)).toBe(expectedText);
+ },
+ );
});
describe.each([
@@ -47,12 +48,12 @@ describe('Error message', () => {
[
mockFilenames(7),
mockFilenames(6),
- 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 1 more.',
+ 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg and 1 more.',
],
[
mockFilenames(8),
mockFilenames(7),
- 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 2 more.',
+ 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg and 2 more.',
],
])('designUploadSkippedWarning', (uploadedFiles, skippedFiles, expected) => {
it('returns expected warning message', () => {
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 9dc82bbdc93..0527c2153f4 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -13,11 +13,8 @@ import DiffFile from '~/diffs/components/diff_file.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
import TreeList from '~/diffs/components/tree_list.vue';
-/* eslint-disable import/order */
-/* You know what: sometimes alphabetical isn't the best order */
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
-/* eslint-enable import/order */
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
@@ -705,4 +702,23 @@ 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/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 0191822d97a..d887029124f 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
+import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
import Component from '~/diffs/components/commit_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
-import getDiffWithCommit from '../mock_data/diff_with_commit';
jest.mock('~/user_popovers');
@@ -18,7 +18,7 @@ describe('diffs/components/commit_item', () => {
let wrapper;
const timeago = getTimeago();
- const { commit } = getDiffWithCommit();
+ const { commit } = getDiffWithCommit;
const getTitleElement = () => wrapper.find('.commit-row-message.item-title');
const getDescElement = () => wrapper.find('pre.commit-row-description');
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 1c0cb1193fa..c48935bc4f0 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -1,11 +1,11 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
import { createStore } from '~/mr_notes/stores';
-import getDiffWithCommit from '../mock_data/diff_with_commit';
import diffsMockData from '../mock_data/merge_request_diffs';
const localVue = createLocalVue();
@@ -22,7 +22,7 @@ describe('CompareVersions', () => {
let wrapper;
let store;
const targetBranchName = 'tmp-wine-dev';
- const { commit } = getDiffWithCommit();
+ const { commit } = getDiffWithCommit;
const createWrapper = (props = {}, commitArgs = {}, createCommit = true) => {
if (createCommit) {
@@ -150,7 +150,7 @@ describe('CompareVersions', () => {
describe('commit', () => {
beforeEach(() => {
- store.state.diffs.commit = getDiffWithCommit().commit;
+ store.state.diffs.commit = getDiffWithCommit.commit;
createWrapper();
});
diff --git a/spec/frontend/diffs/mock_data/diff_with_commit.js b/spec/frontend/diffs/mock_data/diff_with_commit.js
deleted file mode 100644
index f3b39bd3577..00000000000
--- a/spec/frontend/diffs/mock_data/diff_with_commit.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const FIXTURE = 'merge_request_diffs/with_commit.json';
-
-export default function getDiffWithCommit() {
- return getJSONFixture(FIXTURE);
-}
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index b35abc9da02..85734e05aeb 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -51,7 +51,7 @@ import {
} from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
-import * as workerUtils from '~/diffs/utils/workers';
+import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
@@ -253,7 +253,7 @@ describe('DiffsStoreActions', () => {
// Workers are synchronous in Jest environment (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58805)
{
type: types.SET_TREE_DATA,
- payload: workerUtils.generateTreeList(diffMetadata.diff_files),
+ payload: treeWorkerUtils.generateTreeList(diffMetadata.diff_files),
},
],
[],
diff --git a/spec/frontend/diffs/utils/workers_spec.js b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
index 25d8183b777..8113428f712 100644
--- a/spec/frontend/diffs/utils/workers_spec.js
+++ b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
@@ -1,6 +1,10 @@
-import { generateTreeList, getLowestSingleFolder, flattenTree } from '~/diffs/utils/workers';
+import {
+ generateTreeList,
+ getLowestSingleFolder,
+ flattenTree,
+} from '~/diffs/utils/tree_worker_utils';
-describe('~/diffs/utils/workers', () => {
+describe('~/diffs/utils/tree_worker_utils', () => {
describe('generateTreeList', () => {
let files;
diff --git a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
index 07ac080fe08..8a0d1ecf1af 100644
--- a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
@@ -1,7 +1,7 @@
import { languages } from 'monaco-editor';
import { TEST_HOST } from 'helpers/test_constants';
-import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
+import ciSchemaPath from '~/editor/schema/ci.json';
import SourceEditor from '~/editor/source_editor';
const mockRef = 'AABBCCDD';
@@ -84,7 +84,7 @@ describe('~/editor/editor_ci_config_ext', () => {
});
expect(getConfiguredYmlSchema()).toEqual({
- uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
+ uri: `${TEST_HOST}${ciSchemaPath}`,
fileMatch: [defaultBlobPath],
});
});
@@ -99,7 +99,7 @@ describe('~/editor/editor_ci_config_ext', () => {
});
expect(getConfiguredYmlSchema()).toEqual({
- uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
+ uri: `${TEST_HOST}${ciSchemaPath}`,
fileMatch: ['another-ci-filename.yml'],
});
});
diff --git a/spec/frontend/environments/environment_delete_spec.js b/spec/frontend/environments/environment_delete_spec.js
index a8c288a3bd8..2d8cff0c74a 100644
--- a/spec/frontend/environments/environment_delete_spec.js
+++ b/spec/frontend/environments/environment_delete_spec.js
@@ -1,4 +1,4 @@
-import { GlButton } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DeleteComponent from '~/environments/components/environment_delete.vue';
@@ -15,7 +15,7 @@ describe('External URL Component', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findDropdownItem = () => wrapper.find(GlDropdownItem);
beforeEach(() => {
jest.spyOn(window, 'confirm');
@@ -23,14 +23,15 @@ describe('External URL Component', () => {
createWrapper();
});
- it('should render a button to delete the environment', () => {
- expect(findButton().exists()).toBe(true);
- expect(wrapper.attributes('title')).toEqual('Delete environment');
+ it('should render a dropdown item to delete the environment', () => {
+ expect(findDropdownItem().exists()).toBe(true);
+ expect(wrapper.text()).toEqual('Delete environment');
+ expect(findDropdownItem().attributes('variant')).toBe('danger');
});
it('emits requestDeleteEnvironment in the event hub when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
- findButton().vm.$emit('click');
+ findDropdownItem().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('requestDeleteEnvironment', wrapper.vm.environment);
});
});
diff --git a/spec/frontend/environments/environment_monitoring_spec.js b/spec/frontend/environments/environment_monitoring_spec.js
index 3a53b57c3c6..98dd9edd812 100644
--- a/spec/frontend/environments/environment_monitoring_spec.js
+++ b/spec/frontend/environments/environment_monitoring_spec.js
@@ -1,6 +1,6 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import MonitoringComponent from '~/environments/components/environment_monitoring.vue';
+import { __ } from '~/locale';
describe('Monitoring Component', () => {
let wrapper;
@@ -8,31 +8,19 @@ describe('Monitoring Component', () => {
const monitoringUrl = 'https://gitlab.com';
const createWrapper = () => {
- wrapper = shallowMount(MonitoringComponent, {
+ wrapper = mountExtended(MonitoringComponent, {
propsData: {
monitoringUrl,
},
});
};
- const findButtons = () => wrapper.findAll(GlButton);
- const findButtonsByIcon = (icon) =>
- findButtons().filter((button) => button.props('icon') === icon);
-
beforeEach(() => {
createWrapper();
});
- describe('computed', () => {
- it('title', () => {
- expect(wrapper.vm.title).toBe('Monitoring');
- });
- });
-
it('should render a link to environment monitoring page', () => {
- expect(wrapper.attributes('href')).toEqual(monitoringUrl);
- expect(findButtonsByIcon('chart').length).toBe(1);
- expect(wrapper.attributes('title')).toBe('Monitoring');
- expect(wrapper.attributes('aria-label')).toBe('Monitoring');
+ const link = wrapper.findByRole('menuitem', { name: __('Monitoring') });
+ expect(link.attributes('href')).toEqual(monitoringUrl);
});
});
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index 5cdd52294b6..a9a58071e12 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PinComponent from '~/environments/components/environment_pin.vue';
import eventHub from '~/environments/event_hub';
@@ -30,15 +30,15 @@ describe('Pin Component', () => {
wrapper.destroy();
});
- it('should render the component with thumbtack icon', () => {
- expect(wrapper.find(GlIcon).props('name')).toBe('thumbtack');
+ it('should render the component with descriptive text', () => {
+ expect(wrapper.text()).toBe('Prevent auto-stopping');
});
it('should emit onPinClick when clicked', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const button = wrapper.find(GlButton);
+ const item = wrapper.find(GlDropdownItem);
- button.vm.$emit('click');
+ item.vm.$emit('click');
expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
});
diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index b6c3d436c18..cde675cd9e7 100644
--- a/spec/frontend/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -1,5 +1,5 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import RollbackComponent from '~/environments/components/environment_rollback.vue';
import eventHub from '~/environments/event_hub';
@@ -7,7 +7,7 @@ describe('Rollback Component', () => {
const retryUrl = 'https://gitlab.com/retry';
it('Should render Re-deploy label when isLastDeployment is true', () => {
- const wrapper = mount(RollbackComponent, {
+ const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: true,
@@ -15,11 +15,11 @@ describe('Rollback Component', () => {
},
});
- expect(wrapper.element).toHaveSpriteIcon('repeat');
+ expect(wrapper.text()).toBe('Re-deploy to environment');
});
it('Should render Rollback label when isLastDeployment is false', () => {
- const wrapper = mount(RollbackComponent, {
+ const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: false,
@@ -27,7 +27,7 @@ describe('Rollback Component', () => {
},
});
- expect(wrapper.element).toHaveSpriteIcon('redo');
+ expect(wrapper.text()).toBe('Rollback environment');
});
it('should emit a "rollback" event on button click', () => {
@@ -40,7 +40,7 @@ describe('Rollback Component', () => {
},
},
});
- const button = wrapper.find(GlButton);
+ const button = wrapper.find(GlDropdownItem);
button.vm.$emit('click');
diff --git a/spec/frontend/environments/environment_terminal_button_spec.js b/spec/frontend/environments/environment_terminal_button_spec.js
index 2475785a927..ab9f370595f 100644
--- a/spec/frontend/environments/environment_terminal_button_spec.js
+++ b/spec/frontend/environments/environment_terminal_button_spec.js
@@ -1,12 +1,13 @@
-import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import TerminalComponent from '~/environments/components/environment_terminal_button.vue';
+import { __ } from '~/locale';
-describe('Stop Component', () => {
+describe('Terminal Component', () => {
let wrapper;
const terminalPath = '/path';
const mountWithProps = (props) => {
- wrapper = shallowMount(TerminalComponent, {
+ wrapper = mountExtended(TerminalComponent, {
propsData: props,
});
};
@@ -15,17 +16,9 @@ describe('Stop Component', () => {
mountWithProps({ terminalPath });
});
- describe('computed', () => {
- it('title', () => {
- expect(wrapper.vm.title).toEqual('Terminal');
- });
- });
-
it('should render a link to open a web terminal with the provided path', () => {
- expect(wrapper.element.tagName).toBe('A');
- expect(wrapper.attributes('title')).toBe('Terminal');
- expect(wrapper.attributes('aria-label')).toBe('Terminal');
- expect(wrapper.attributes('href')).toBe(terminalPath);
+ const link = wrapper.findByRole('menuitem', { name: __('Terminal') });
+ expect(link.attributes('href')).toBe(terminalPath);
});
it('should render a non-disabled button', () => {
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index babbc0c8a4d..4e459d800e8 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -503,6 +503,53 @@ describe('ErrorDetails', () => {
});
});
});
+
+ describe('Release links', () => {
+ const firstReleaseVersion = '7975be01';
+ const firstCommitLink = '/gitlab/-/commit/7975be01';
+ const firstReleaseLink = '/sentry/releases/7975be01';
+ const findFirstCommitLink = () => wrapper.find(`[href$="${firstCommitLink}"]`);
+ const findFirstReleaseLink = () => wrapper.find(`[href$="${firstReleaseLink}"]`);
+
+ const lastReleaseVersion = '6ca5a5c1';
+ const lastCommitLink = '/gitlab/-/commit/6ca5a5c1';
+ const lastReleaseLink = '/sentry/releases/6ca5a5c1';
+ const findLastCommitLink = () => wrapper.find(`[href$="${lastCommitLink}"]`);
+ const findLastReleaseLink = () => wrapper.find(`[href$="${lastReleaseLink}"]`);
+
+ it('should display links to Sentry', async () => {
+ mocks.$apollo.queries.error.loading = false;
+ await wrapper.setData({
+ error: {
+ firstReleaseVersion,
+ lastReleaseVersion,
+ externalBaseUrl: '/sentry',
+ },
+ });
+
+ expect(findFirstReleaseLink().exists()).toBe(true);
+ expect(findLastReleaseLink().exists()).toBe(true);
+ expect(findFirstCommitLink().exists()).toBe(false);
+ expect(findLastCommitLink().exists()).toBe(false);
+ });
+
+ it('should display links to GitLab when integrated', async () => {
+ mocks.$apollo.queries.error.loading = false;
+ await wrapper.setData({
+ error: {
+ firstReleaseVersion,
+ lastReleaseVersion,
+ integrated: true,
+ externalBaseUrl: '/gitlab',
+ },
+ });
+
+ expect(findFirstCommitLink().exists()).toBe(true);
+ expect(findLastCommitLink().exists()).toBe(true);
+ expect(findFirstReleaseLink().exists()).toBe(false);
+ expect(findLastReleaseLink().exists()).toBe(false);
+ });
+ });
});
describe('Snowplow tracking', () => {
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index 30541ba68a5..844faff64a1 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -1,7 +1,8 @@
-import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import { GlFormRadioGroup, GlFormRadio, GlFormInputGroup } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue';
@@ -12,6 +13,8 @@ import createStore from '~/error_tracking_settings/store';
const localVue = createLocalVue();
localVue.use(Vuex);
+const TEST_GITLAB_DSN = 'https://gitlab.example.com/123456';
+
describe('error tracking settings app', () => {
let store;
let wrapper;
@@ -29,6 +32,10 @@ describe('error tracking settings app', () => {
initialProject: null,
listProjectsEndpoint: TEST_HOST,
operationsSettingsEndpoint: TEST_HOST,
+ gitlabDsn: TEST_GITLAB_DSN,
+ },
+ stubs: {
+ GlFormInputGroup, // we need this non-shallow to query for a component within a slot
},
}),
);
@@ -41,6 +48,12 @@ describe('error tracking settings app', () => {
findBackendSettingsRadioGroup().findAllComponents(GlFormRadio);
const findElementWithText = (wrappers, text) => wrappers.filter((item) => item.text() === text);
const findSentrySettings = () => wrapper.findByTestId('sentry-setting-form');
+ const findDsnSettings = () => wrapper.findByTestId('gitlab-dsn-setting-form');
+
+ const enableGitLabErrorTracking = async () => {
+ findBackendSettingsRadioGroup().vm.$emit('change', true);
+ await nextTick();
+ };
beforeEach(() => {
store = createStore();
@@ -93,17 +106,35 @@ describe('error tracking settings app', () => {
expect(findElementWithText(findBackendSettingsRadioButtons(), 'GitLab')).toHaveLength(1);
});
- it('toggles the sentry-settings section when sentry is selected as a tracking-backend', async () => {
+ it('hides the Sentry settings when GitLab is selected as a tracking-backend', async () => {
expect(findSentrySettings().exists()).toBe(true);
- // set the "integrated" setting to "true"
- findBackendSettingsRadioGroup().vm.$emit('change', true);
-
- await nextTick();
+ await enableGitLabErrorTracking();
expect(findSentrySettings().exists()).toBe(false);
});
+ describe('GitLab DSN section', () => {
+ it('is visible when GitLab is selected as a tracking-backend and DSN is present', async () => {
+ expect(findDsnSettings().exists()).toBe(false);
+
+ await enableGitLabErrorTracking();
+
+ expect(findDsnSettings().exists()).toBe(true);
+ });
+
+ it('contains copy-to-clipboard functionality for the GitLab DSN string', async () => {
+ await enableGitLabErrorTracking();
+
+ const clipBoardInput = findDsnSettings().findComponent(GlFormInputGroup);
+ const clipBoardButton = findDsnSettings().findComponent(ClipboardButton);
+
+ expect(clipBoardInput.props('value')).toBe(TEST_GITLAB_DSN);
+ expect(clipBoardInput.attributes('readonly')).toBeTruthy();
+ expect(clipBoardButton.props('text')).toBe(TEST_GITLAB_DSN);
+ });
+ });
+
it.each([true, false])(
'calls the `updateIntegrated` action when the setting changes to `%s`',
(integrated) => {
diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js
index 999bed1ffbd..de060f5eb8c 100644
--- a/spec/frontend/experimentation/utils_spec.js
+++ b/spec/frontend/experimentation/utils_spec.js
@@ -23,20 +23,6 @@ describe('experiment Utilities', () => {
});
});
- describe('getExperimentContexts', () => {
- describe.each`
- gon | input | output
- ${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${[{ schema: TRACKING_CONTEXT_SCHEMA, data: { variant: '_data_' } }]}
- ${[]} | ${[TEST_KEY]} | ${[]}
- `('with input=$input and gon=$gon', ({ gon, input, output }) => {
- assignGitlabExperiment(...gon);
-
- it(`returns ${output}`, () => {
- expect(experimentUtils.getExperimentContexts(...input)).toEqual(output);
- });
- });
- });
-
describe('getAllExperimentContexts', () => {
const schema = TRACKING_CONTEXT_SCHEMA;
let origGon;
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index 799b567a2c0..721b7249abc 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import EditFeatureFlag from '~/feature_flags/components/edit_feature_flag.vue';
import Form from '~/feature_flags/components/form.vue';
@@ -20,7 +21,7 @@ describe('Edit feature flag form', () => {
endpoint: `${TEST_HOST}/feature_flags.json`,
});
- const factory = (provide = {}) => {
+ const factory = (provide = { searchPath: '/search' }) => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
@@ -31,7 +32,7 @@ describe('Edit feature flag form', () => {
});
};
- beforeEach((done) => {
+ beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`${TEST_HOST}/feature_flags.json`).replyOnce(200, {
id: 21,
@@ -45,7 +46,8 @@ describe('Edit feature flag form', () => {
destroy_path: '/h5bp/html5-boilerplate/-/feature_flags/21',
});
factory();
- setImmediate(() => done());
+
+ return waitForPromises();
});
afterEach(() => {
@@ -60,7 +62,7 @@ describe('Edit feature flag form', () => {
});
it('should render the toggle', () => {
- expect(wrapper.find(GlToggle).exists()).toBe(true);
+ expect(wrapper.findComponent(GlToggle).exists()).toBe(true);
});
describe('with error', () => {
@@ -80,11 +82,11 @@ describe('Edit feature flag form', () => {
});
it('should render feature flag form', () => {
- expect(wrapper.find(Form).exists()).toEqual(true);
+ expect(wrapper.findComponent(Form).exists()).toEqual(true);
});
it('should track when the toggle is clicked', () => {
- const toggle = wrapper.find(GlToggle);
+ const toggle = wrapper.findComponent(GlToggle);
const spy = mockTracking('_category_', toggle.element, jest.spyOn);
toggle.trigger('click');
@@ -95,7 +97,7 @@ describe('Edit feature flag form', () => {
});
it('should render the toggle with a visually hidden label', () => {
- expect(wrapper.find(GlToggle).props()).toMatchObject({
+ expect(wrapper.findComponent(GlToggle).props()).toMatchObject({
label: 'Feature flag status',
labelPosition: 'hidden',
});
diff --git a/spec/frontend/filterable_list_spec.js b/spec/frontend/filterable_list_spec.js
index 8c6a71abad7..556cf6f8137 100644
--- a/spec/frontend/filterable_list_spec.js
+++ b/spec/frontend/filterable_list_spec.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-deprecated
import { getJSONFixture, setHTMLFixture } from 'helpers/fixtures';
import FilterableList from '~/filterable_list';
@@ -14,6 +15,7 @@ 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');
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index 961587f7146..9a20fb1bae6 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -1,8 +1,5 @@
-import DropdownUtils from '~/filtered_search/dropdown_utils';
-// TODO: Moving this line up throws an error about `FilteredSearchDropdown`
-// being undefined in test. See gitlab-org/gitlab#321476 for more info.
-// eslint-disable-next-line import/order
import DropdownUser from '~/filtered_search/dropdown_user';
+import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
diff --git a/spec/frontend/droplab/constants_spec.js b/spec/frontend/filtered_search/droplab/constants_spec.js
index fd48228d6a2..9c1caf90ac0 100644
--- a/spec/frontend/droplab/constants_spec.js
+++ b/spec/frontend/filtered_search/droplab/constants_spec.js
@@ -1,4 +1,4 @@
-import * as constants from '~/droplab/constants';
+import * as constants from '~/filtered_search/droplab/constants';
describe('constants', () => {
describe('DATA_TRIGGER', () => {
diff --git a/spec/frontend/droplab/drop_down_spec.js b/spec/frontend/filtered_search/droplab/drop_down_spec.js
index dcdbbcd4ccf..f49dbfcf79c 100644
--- a/spec/frontend/droplab/drop_down_spec.js
+++ b/spec/frontend/filtered_search/droplab/drop_down_spec.js
@@ -1,6 +1,6 @@
-import { SELECTED_CLASS } from '~/droplab/constants';
-import DropDown from '~/droplab/drop_down';
-import utils from '~/droplab/utils';
+import { SELECTED_CLASS } from '~/filtered_search/droplab/constants';
+import DropDown from '~/filtered_search/droplab/drop_down';
+import utils from '~/filtered_search/droplab/utils';
describe('DropLab DropDown', () => {
let testContext;
diff --git a/spec/frontend/droplab/hook_spec.js b/spec/frontend/filtered_search/droplab/hook_spec.js
index 0b897a570f6..0d92170cfcf 100644
--- a/spec/frontend/droplab/hook_spec.js
+++ b/spec/frontend/filtered_search/droplab/hook_spec.js
@@ -1,7 +1,7 @@
-import DropDown from '~/droplab/drop_down';
-import Hook from '~/droplab/hook';
+import DropDown from '~/filtered_search/droplab/drop_down';
+import Hook from '~/filtered_search/droplab/hook';
-jest.mock('~/droplab/drop_down', () => jest.fn());
+jest.mock('~/filtered_search/droplab/drop_down', () => jest.fn());
describe('Hook', () => {
let testContext;
diff --git a/spec/frontend/droplab/plugins/ajax_filter_spec.js b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
index d442d5cf416..88b3fc236e4 100644
--- a/spec/frontend/droplab/plugins/ajax_filter_spec.js
+++ b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
@@ -1,4 +1,4 @@
-import AjaxFilter from '~/droplab/plugins/ajax_filter';
+import AjaxFilter from '~/filtered_search/droplab/plugins/ajax_filter';
import AjaxCache from '~/lib/utils/ajax_cache';
describe('AjaxFilter', () => {
diff --git a/spec/frontend/droplab/plugins/ajax_spec.js b/spec/frontend/filtered_search/droplab/plugins/ajax_spec.js
index 7c6452e8337..c968b982091 100644
--- a/spec/frontend/droplab/plugins/ajax_spec.js
+++ b/spec/frontend/filtered_search/droplab/plugins/ajax_spec.js
@@ -1,4 +1,4 @@
-import Ajax from '~/droplab/plugins/ajax';
+import Ajax from '~/filtered_search/droplab/plugins/ajax';
import AjaxCache from '~/lib/utils/ajax_cache';
describe('Ajax', () => {
diff --git a/spec/frontend/droplab/plugins/input_setter_spec.js b/spec/frontend/filtered_search/droplab/plugins/input_setter_spec.js
index eebde018fa1..811b5ca4573 100644
--- a/spec/frontend/droplab/plugins/input_setter_spec.js
+++ b/spec/frontend/filtered_search/droplab/plugins/input_setter_spec.js
@@ -1,4 +1,4 @@
-import InputSetter from '~/droplab/plugins/input_setter';
+import InputSetter from '~/filtered_search/droplab/plugins/input_setter';
describe('InputSetter', () => {
let testContext;
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index 7185f382fc1..8ac5b6fbea6 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -1,4 +1,5 @@
import { escape } from 'lodash';
+import labelData from 'test_fixtures/labels/project_labels.json';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
import DropdownUtils from '~/filtered_search/dropdown_utils';
@@ -132,15 +133,8 @@ describe('Filtered Search Visual Tokens', () => {
});
describe('updateLabelTokenColor', () => {
- const jsonFixtureName = 'labels/project_labels.json';
const dummyEndpoint = '/dummy/endpoint';
- let labelData;
-
- beforeAll(() => {
- labelData = getJSONFixture(jsonFixtureName);
- });
-
const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
'label',
'=',
diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb
index f5524a10033..d8c8737b125 100644
--- a/spec/frontend/fixtures/abuse_reports.rb
+++ b/spec/frontend/fixtures/abuse_reports.rb
@@ -13,10 +13,6 @@ RSpec.describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :co
render_views
- before(:all) do
- clean_frontend_fixtures('abuse_reports/')
- end
-
before do
sign_in(admin)
enable_admin_mode!(admin)
diff --git a/spec/frontend/fixtures/admin_users.rb b/spec/frontend/fixtures/admin_users.rb
index e0fecbdb1aa..5579f50da74 100644
--- a/spec/frontend/fixtures/admin_users.rb
+++ b/spec/frontend/fixtures/admin_users.rb
@@ -17,10 +17,6 @@ RSpec.describe Admin::UsersController, '(JavaScript fixtures)', type: :controlle
render_views
- before(:all) do
- clean_frontend_fixtures('admin/users')
- end
-
it 'admin/users/new_with_internal_user_regex.html' do
stub_application_setting(user_default_external: true)
stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?')
diff --git a/spec/frontend/fixtures/analytics.rb b/spec/frontend/fixtures/analytics.rb
index 6d106dce166..b6a5ea6616d 100644
--- a/spec/frontend/fixtures/analytics.rb
+++ b/spec/frontend/fixtures/analytics.rb
@@ -6,10 +6,6 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
let_it_be(:value_stream_id) { 'default' }
- before(:all) do
- clean_frontend_fixtures('projects/analytics/value_stream_analytics/')
- end
-
before do
update_metrics
create_deployment
@@ -26,7 +22,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
sign_in(user)
end
- it 'projects/analytics/value_stream_analytics/stages' do
+ it 'projects/analytics/value_stream_analytics/stages.json' do
get(:index, params: params, format: :json)
expect(response).to be_successful
@@ -44,7 +40,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
end
Gitlab::Analytics::CycleAnalytics::DefaultStages.all.each do |stage|
- it "projects/analytics/value_stream_analytics/events/#{stage[:name]}" do
+ it "projects/analytics/value_stream_analytics/events/#{stage[:name]}.json" do
get(stage[:name], params: params, format: :json)
expect(response).to be_successful
@@ -62,7 +58,7 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
sign_in(user)
end
- it "projects/analytics/value_stream_analytics/summary" do
+ it "projects/analytics/value_stream_analytics/summary.json" do
get(:show, params: params, format: :json)
expect(response).to be_successful
diff --git a/spec/frontend/fixtures/api_markdown.rb b/spec/frontend/fixtures/api_markdown.rb
index cb9a116f293..89f012a5110 100644
--- a/spec/frontend/fixtures/api_markdown.rb
+++ b/spec/frontend/fixtures/api_markdown.rb
@@ -21,11 +21,7 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
let(:project_wiki_page) { create(:wiki_page, wiki: project_wiki) }
- fixture_subdir = 'api/markdown'
-
before(:all) do
- clean_frontend_fixtures(fixture_subdir)
-
group.add_owner(user)
project.add_maintainer(user)
end
@@ -49,7 +45,7 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
name = "#{context}_#{name}" unless context.empty?
- it "#{fixture_subdir}/#{name}.json" do
+ it "api/markdown/#{name}.json" do
api_url = case context
when 'project'
"/#{project.full_path}/preview_markdown"
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 1edb8cb3f41..45f73260887 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -77,6 +77,35 @@
</dd>
</dl>
+- name: details
+ markdown: |-
+ <details>
+ <summary>Apply this patch</summary>
+
+ ```diff
+ diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
+ index 8433efaf00c..69b12c59d46 100644
+ --- a/spec/frontend/fixtures/api_markdown.yml
+ +++ b/spec/frontend/fixtures/api_markdown.yml
+ @@ -33,6 +33,13 @@
+ * <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>.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: details
+ + markdown: |-
+ + <details>
+ + <summary>Apply this patch</summary>
+ +
+ + 🐶 much meta, 🐶 many patch
+ + 🐶 such diff, 🐶 very meme
+ + 🐶 wow!
+ + </details>
+ - name: link
+ markdown: '[GitLab](https://gitlab.com)'
+ - name: attachment_link
+ ```
+
+ </details>
- name: link
markdown: '[GitLab](https://gitlab.com)'
- name: attachment_link
@@ -204,3 +233,57 @@
* [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_]]
+
+ # Lorem
+
+ Well, that's just like... your opinion.. man.
+
+ ## Ipsum
+
+ ### Dolar
+
+ # 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
+ 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
+ markdown: |-
+ This math is inline $`a^2+b^2=c^2`$.
+
+ This is on a separate line:
+
+ ```math
+ a^2+b^2=c^2
+ ```
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index 7117c9a1c7a..47321fbbeaa 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -11,10 +11,6 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let_it_be(:mr) { create(:merge_request, source_project: project) }
- before(:all) do
- clean_frontend_fixtures('api/merge_requests')
- end
-
it 'api/merge_requests/get.json' do
4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index fa77ca1c0cf..eada2f8e0f7 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -11,10 +11,6 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
- before(:all) do
- clean_frontend_fixtures('api/projects')
- end
-
it 'api/projects/get.json' do
get api("/projects/#{project.id}", admin)
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index b09bea56b94..9fa8d68e695 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -19,10 +19,6 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
render_views
- before(:all) do
- clean_frontend_fixtures('application_settings/')
- end
-
after do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/autocomplete.rb b/spec/frontend/fixtures/autocomplete.rb
index 8983e241aa5..6215fa44e27 100644
--- a/spec/frontend/fixtures/autocomplete.rb
+++ b/spec/frontend/fixtures/autocomplete.rb
@@ -11,10 +11,6 @@ RSpec.describe ::AutocompleteController, '(JavaScript fixtures)', type: :control
let(:project) { create(:project, namespace: group, path: 'autocomplete-project') }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
- before(:all) do
- clean_frontend_fixtures('autocomplete/')
- end
-
before do
group.add_owner(user)
sign_in(user)
diff --git a/spec/frontend/fixtures/autocomplete_sources.rb b/spec/frontend/fixtures/autocomplete_sources.rb
index 9ff0f959c11..74bf58cc106 100644
--- a/spec/frontend/fixtures/autocomplete_sources.rb
+++ b/spec/frontend/fixtures/autocomplete_sources.rb
@@ -10,10 +10,6 @@ RSpec.describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)',
let_it_be(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') }
let_it_be(:issue) { create(:issue, project: project) }
- before(:all) do
- clean_frontend_fixtures('autocomplete_sources/')
- end
-
before do
group.add_owner(user)
sign_in(user)
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index b112886b2ca..f90e3662e98 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -11,10 +11,6 @@ RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :control
render_views
- before(:all) do
- clean_frontend_fixtures('blob/')
- end
-
before do
sign_in(user)
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index f3b3633347d..828564977e0 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -9,11 +9,6 @@ RSpec.describe 'Branches (JavaScript fixtures)' do
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let_it_be(:user) { project.owner }
- before(:all) do
- clean_frontend_fixtures('branches/')
- clean_frontend_fixtures('api/branches/')
- end
-
after(:all) do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb
index b37aa137504..ea883555255 100644
--- a/spec/frontend/fixtures/clusters.rb
+++ b/spec/frontend/fixtures/clusters.rb
@@ -12,10 +12,6 @@ RSpec.describe Projects::ClustersController, '(JavaScript fixtures)', type: :con
render_views
- before(:all) do
- clean_frontend_fixtures('clusters/')
- end
-
before do
sign_in(user)
end
diff --git a/spec/frontend/fixtures/commit.rb b/spec/frontend/fixtures/commit.rb
index ff62a8286fc..f9e0f604b52 100644
--- a/spec/frontend/fixtures/commit.rb
+++ b/spec/frontend/fixtures/commit.rb
@@ -9,11 +9,6 @@ RSpec.describe 'Commit (JavaScript fixtures)' do
let_it_be(:user) { project.owner }
let_it_be(:commit) { project.commit("master") }
- before(:all) do
- clean_frontend_fixtures('commit/')
- clean_frontend_fixtures('api/commits/')
- end
-
before do
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb
index 5c24c071792..bed6c798793 100644
--- a/spec/frontend/fixtures/deploy_keys.rb
+++ b/spec/frontend/fixtures/deploy_keys.rb
@@ -13,10 +13,6 @@ RSpec.describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :c
let(:project3) { create(:project, :internal)}
let(:project4) { create(:project, :internal)}
- before(:all) do
- clean_frontend_fixtures('deploy_keys/')
- end
-
before do
# Using an admin for these fixtures because they are used for verifying a frontend
# component that would normally get its data from `Admin::DeployKeysController`
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
index 42762fa56f9..d9573c8000d 100644
--- a/spec/frontend/fixtures/freeze_period.rb
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -9,10 +9,6 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') }
let_it_be(:user) { project.owner }
- before(:all) do
- clean_frontend_fixtures('api/freeze-periods/')
- end
-
after(:all) do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb
index 42aad9f187e..ddd436b98c6 100644
--- a/spec/frontend/fixtures/groups.rb
+++ b/spec/frontend/fixtures/groups.rb
@@ -8,10 +8,6 @@ RSpec.describe 'Groups (JavaScript fixtures)', type: :controller do
let(:user) { create(:user) }
let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')}
- before(:all) do
- clean_frontend_fixtures('groups/')
- end
-
before do
group.add_owner(user)
sign_in(user)
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index a027247bd0d..6519416cb9e 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -11,10 +11,6 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
render_views
- before(:all) do
- clean_frontend_fixtures('issues/')
- end
-
before do
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 22179c790bd..12584f38629 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -21,10 +21,6 @@ RSpec.describe Projects::JobsController, '(JavaScript fixtures)', type: :control
render_views
- before(:all) do
- clean_frontend_fixtures('jobs/')
- end
-
before do
sign_in(user)
end
diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb
index d7ca2aff18c..6736baed199 100644
--- a/spec/frontend/fixtures/labels.rb
+++ b/spec/frontend/fixtures/labels.rb
@@ -17,10 +17,6 @@ RSpec.describe 'Labels (JavaScript fixtures)' do
let!(:groub_label_space) { create(:group_label, group: group, title: 'some space', color: '#FFFFFF') }
let!(:groub_label_violets) { create(:group_label, group: group, title: 'violets', color: '#0000FF') }
- before(:all) do
- clean_frontend_fixtures('labels/')
- end
-
after do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index f10f96f2516..68ed2ca2359 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -49,10 +49,6 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
render_views
- before(:all) do
- clean_frontend_fixtures('merge_requests/')
- end
-
before do
sign_in(user)
allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo'])
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index edf1fcf3c0a..e733764f248 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -20,10 +20,6 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
render_views
- before(:all) do
- clean_frontend_fixtures('merge_request_diffs/')
- end
-
before do
# Create a user that matches the project.commit author
# This is so that the "author" information will be populated
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
index eef79825ae7..d59b01b04af 100644
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ b/spec/frontend/fixtures/metrics_dashboard.rb
@@ -12,10 +12,6 @@ RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
let_it_be(:environment) { create(:environment, id: 1, project: project) }
let_it_be(:params) { { environment: environment } }
- before(:all) do
- clean_frontend_fixtures('metrics_dashboard/')
- end
-
controller(::ApplicationController) do
include MetricsDashboard
end
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index a7d43fdbe62..6389f59aa0a 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -15,10 +15,6 @@ RSpec.describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', t
render_views
- before(:all) do
- clean_frontend_fixtures('pipeline_schedules/')
- end
-
before do
sign_in(user)
end
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index f695b74ec87..709e14183df 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -23,10 +23,6 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:build_test) { create(:ci_build, pipeline: pipeline, stage: 'test') }
let!(:build_deploy_failed) { create(:ci_build, status: :failed, pipeline: pipeline, stage: 'deploy') }
- before(:all) do
- clean_frontend_fixtures('pipelines/')
- end
-
before do
sign_in(user)
end
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 7873d59dbad..3c8964d398a 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -16,10 +16,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
render_views
- before(:all) do
- clean_frontend_fixtures('projects/')
- end
-
before do
project_with_repo.add_maintainer(user)
sign_in(user)
@@ -57,10 +53,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
project_variable_populated.add_maintainer(user)
end
- before(:all) do
- clean_frontend_fixtures('graphql/projects/access_tokens')
- end
-
base_input_path = 'access_tokens/graphql/queries/'
base_output_path = 'graphql/projects/access_tokens/'
query_name = 'get_projects.query.graphql'
diff --git a/spec/frontend/fixtures/projects_json.rb b/spec/frontend/fixtures/projects_json.rb
index c081d4f08dc..c4de56ccfab 100644
--- a/spec/frontend/fixtures/projects_json.rb
+++ b/spec/frontend/fixtures/projects_json.rb
@@ -8,10 +8,6 @@ RSpec.describe 'Projects JSON endpoints (JavaScript fixtures)', type: :controlle
let(:admin) { create(:admin, name: 'root') }
let(:project) { create(:project, :repository) }
- before(:all) do
- clean_frontend_fixtures('projects_json/')
- end
-
before do
project.add_maintainer(admin)
sign_in(admin)
diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_service.rb
index c349f2a24bc..bbd938c66f6 100644
--- a/spec/frontend/fixtures/prometheus_service.rb
+++ b/spec/frontend/fixtures/prometheus_service.rb
@@ -12,10 +12,6 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
render_views
- before(:all) do
- clean_frontend_fixtures('services/prometheus')
- end
-
before do
sign_in(user)
end
diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb
index 44927bd29d8..211c4e7c048 100644
--- a/spec/frontend/fixtures/raw.rb
+++ b/spec/frontend/fixtures/raw.rb
@@ -9,14 +9,6 @@ RSpec.describe 'Raw files', '(JavaScript fixtures)' do
let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') }
let(:response) { @blob.data.force_encoding('UTF-8') }
- before(:all) do
- clean_frontend_fixtures('blob/notebook/')
- clean_frontend_fixtures('blob/pdf/')
- clean_frontend_fixtures('blob/text/')
- clean_frontend_fixtures('blob/binary/')
- clean_frontend_fixtures('blob/images/')
- end
-
after do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/releases.rb b/spec/frontend/fixtures/releases.rb
index e8f259fba15..fc344472588 100644
--- a/spec/frontend/fixtures/releases.rb
+++ b/spec/frontend/fixtures/releases.rb
@@ -116,10 +116,6 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
end
describe API::Releases, type: :request do
- before(:all) do
- clean_frontend_fixtures('api/releases/')
- end
-
it 'api/releases/release.json' do
get api("/projects/#{project.id}/releases/#{release.tag}", admin)
@@ -134,10 +130,6 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
one_release_query_path = 'releases/graphql/queries/one_release.query.graphql'
one_release_for_editing_query_path = 'releases/graphql/queries/one_release_for_editing.query.graphql'
- before(:all) do
- clean_frontend_fixtures('graphql/releases/')
- end
-
it "graphql/#{all_releases_query_path}.json" do
query = get_graphql_query_as_string(all_releases_query_path)
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index d5d6f534def..fa150fbf57c 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -20,10 +20,6 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
query_path = 'runner/graphql/'
fixtures_path = 'graphql/runner/'
- before(:all) do
- clean_frontend_fixtures(fixtures_path)
- end
-
after(:all) do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb
index 264ce7d010c..db1ef67998f 100644
--- a/spec/frontend/fixtures/search.rb
+++ b/spec/frontend/fixtures/search.rb
@@ -9,10 +9,6 @@ RSpec.describe SearchController, '(JavaScript fixtures)', type: :controller do
let_it_be(:user) { create(:user) }
- before(:all) do
- clean_frontend_fixtures('search/')
- end
-
before do
sign_in(user)
end
diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/services.rb
index 91e6c2eb280..a8293a080a9 100644
--- a/spec/frontend/fixtures/services.rb
+++ b/spec/frontend/fixtures/services.rb
@@ -12,10 +12,6 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
render_views
- before(:all) do
- clean_frontend_fixtures('services/')
- end
-
before do
sign_in(user)
end
diff --git a/spec/frontend/fixtures/sessions.rb b/spec/frontend/fixtures/sessions.rb
index 0ef14c1d4fa..bb73bf3215c 100644
--- a/spec/frontend/fixtures/sessions.rb
+++ b/spec/frontend/fixtures/sessions.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
RSpec.describe 'Sessions (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- before(:all) do
- clean_frontend_fixtures('sessions/')
- end
-
describe SessionsController, '(JavaScript fixtures)', type: :controller do
include DeviseHelpers
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 5211d52f374..397fb3e7124 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -12,10 +12,6 @@ RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
render_views
- before(:all) do
- clean_frontend_fixtures('snippets/')
- end
-
before do
sign_in(user)
allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo'])
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
index 1bd99f5cd7f..e19a98c3bab 100644
--- a/spec/frontend/fixtures/startup_css.rb
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -9,15 +9,15 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
render_views
- before(:all) do
- clean_frontend_fixtures('startup_css/')
- end
-
shared_examples 'startup css project fixtures' do |type|
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :public, :repository, description: 'Code and stuff', creator: user) }
before do
+ # We want vNext badge to be included and com/canary don't remove/hide any other elements.
+ # This is why we're turning com and canary on by default for now.
+ allow(Gitlab).to receive(:com?).and_return(true)
+ allow(Gitlab).to receive(:canary?).and_return(true)
sign_in(user)
end
diff --git a/spec/frontend/fixtures/static/oauth_remember_me.html b/spec/frontend/fixtures/static/oauth_remember_me.html
index c6af8129b4d..0b4d482925d 100644
--- a/spec/frontend/fixtures/static/oauth_remember_me.html
+++ b/spec/frontend/fixtures/static/oauth_remember_me.html
@@ -1,22 +1,21 @@
<div id="oauth-container">
-<input id="remember_me" type="checkbox">
+ <input id="remember_me" type="checkbox" />
-<form method="post" action="http://example.com/">
- <button class="oauth-login twitter" type="submit">
- <span>Twitter</span>
- </button>
-</form>
+ <form method="post" action="http://example.com/">
+ <button class="js-oauth-login twitter" type="submit">
+ <span>Twitter</span>
+ </button>
+ </form>
-<form method="post" action="http://example.com/">
- <button class="oauth-login github" type="submit">
- <span>GitHub</span>
- </button>
-</form>
-
-<form method="post" action="http://example.com/?redirect_fragment=L1">
- <button class="oauth-login facebook" type="submit">
- <span>Facebook</span>
- </button>
-</form>
+ <form method="post" action="http://example.com/">
+ <button class="js-oauth-login github" type="submit">
+ <span>GitHub</span>
+ </button>
+ </form>
+ <form method="post" action="http://example.com/?redirect_fragment=L1">
+ <button class="js-oauth-login facebook" type="submit">
+ <span>Facebook</span>
+ </button>
+ </form>
</div>
diff --git a/spec/frontend/fixtures/tags.rb b/spec/frontend/fixtures/tags.rb
index 9483f0a4492..6cfa5f82efe 100644
--- a/spec/frontend/fixtures/tags.rb
+++ b/spec/frontend/fixtures/tags.rb
@@ -8,10 +8,6 @@ RSpec.describe 'Tags (JavaScript fixtures)' do
let_it_be(:project) { create(:project, :repository, path: 'tags-project') }
let_it_be(:user) { project.owner }
- before(:all) do
- clean_frontend_fixtures('api/tags/')
- end
-
after(:all) do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/timezones.rb b/spec/frontend/fixtures/timezones.rb
index 261dcf5e116..157f47855ea 100644
--- a/spec/frontend/fixtures/timezones.rb
+++ b/spec/frontend/fixtures/timezones.rb
@@ -8,10 +8,6 @@ RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do
let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json }
- before(:all) do
- clean_frontend_fixtures('timezones/')
- end
-
it 'timezones/short.json' do
@timezones = timezone_data(format: :short)
end
diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb
index 985afafe50e..a0573b0b658 100644
--- a/spec/frontend/fixtures/todos.rb
+++ b/spec/frontend/fixtures/todos.rb
@@ -13,10 +13,6 @@ RSpec.describe 'Todos (JavaScript fixtures)' do
let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
let!(:todo_2) { create(:todo, :done, user: user, project: project, target: issue_2, created_at: 50.hours.ago) }
- before(:all) do
- clean_frontend_fixtures('todos/')
- end
-
after do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
index a6a8ba7318b..96820c9ae80 100644
--- a/spec/frontend/fixtures/u2f.rb
+++ b/spec/frontend/fixtures/u2f.rb
@@ -7,10 +7,6 @@ RSpec.context 'U2F' do
let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') }
- before(:all) do
- clean_frontend_fixtures('u2f/')
- end
-
before do
stub_feature_flags(webauthn: false)
end
diff --git a/spec/frontend/fixtures/webauthn.rb b/spec/frontend/fixtures/webauthn.rb
index b195fee76f0..c6e9b41b584 100644
--- a/spec/frontend/fixtures/webauthn.rb
+++ b/spec/frontend/fixtures/webauthn.rb
@@ -7,10 +7,6 @@ RSpec.context 'WebAuthn' do
let(:user) { create(:user, :two_factor_via_webauthn, otp_secret: 'otpsecret:coolkids') }
- before(:all) do
- clean_frontend_fixtures('webauthn/')
- end
-
describe SessionsController, '(JavaScript fixtures)', type: :controller do
include DeviseHelpers
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 94ad7759110..eb11df2fe43 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,17 +1,15 @@
/* eslint no-param-reassign: "off" */
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 { initEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
-import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
-const labelsFixture = getJSONFixture('autocomplete_sources/labels.json');
-
describe('GfmAutoComplete', () => {
const fetchDataMock = { fetchData: jest.fn() };
let gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call(fetchDataMock);
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index 2cbcb73ce5b..2ea2693a978 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import HeaderSearchApp from '~/header_search/components/app.vue';
+import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue';
import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
import { ENTER_KEY, ESC_KEY } from '~/lib/utils/keys';
@@ -20,6 +21,7 @@ describe('HeaderSearchApp', () => {
const actionSpies = {
setSearch: jest.fn(),
+ fetchAutocompleteOptions: jest.fn(),
};
const createComponent = (initialState) => {
@@ -46,6 +48,8 @@ describe('HeaderSearchApp', () => {
const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu');
const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems);
const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems);
+ const findHeaderSearchAutocompleteItems = () =>
+ wrapper.findComponent(HeaderSearchAutocompleteItems);
describe('template', () => {
it('always renders Header Search Input', () => {
@@ -74,11 +78,11 @@ describe('HeaderSearchApp', () => {
});
describe.each`
- search | showDefault | showScoped
- ${null} | ${true} | ${false}
- ${''} | ${true} | ${false}
- ${MOCK_SEARCH} | ${false} | ${true}
- `('Header Search Dropdown Items', ({ search, showDefault, showScoped }) => {
+ search | showDefault | showScoped | showAutocomplete
+ ${null} | ${true} | ${false} | ${false}
+ ${''} | ${true} | ${false} | ${false}
+ ${MOCK_SEARCH} | ${false} | ${true} | ${true}
+ `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => {
describe(`when search is ${search}`, () => {
beforeEach(() => {
createComponent({ search });
@@ -93,6 +97,10 @@ describe('HeaderSearchApp', () => {
it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => {
expect(findHeaderSearchScopedItems().exists()).toBe(showScoped);
});
+
+ it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => {
+ expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete);
+ });
});
});
});
@@ -139,12 +147,18 @@ describe('HeaderSearchApp', () => {
});
});
- it('calls setSearch when search input event is fired', async () => {
- findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH);
+ describe('onInput', () => {
+ beforeEach(() => {
+ findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH);
+ });
- await wrapper.vm.$nextTick();
+ it('calls setSearch with search term', () => {
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH);
+ });
- expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH);
+ it('calls fetchAutocompleteOptions', () => {
+ expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled();
+ });
});
it('submits a search onKey-Enter', async () => {
diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
new file mode 100644
index 00000000000..6b84e63989d
--- /dev/null
+++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
@@ -0,0 +1,108 @@
+import { GlDropdownItem, GlLoadingIcon, GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
+import {
+ GROUPS_CATEGORY,
+ LARGE_AVATAR_PX,
+ PROJECTS_CATEGORY,
+ SMALL_AVATAR_PX,
+} from '~/header_search/constants';
+import { MOCK_GROUPED_AUTOCOMPLETE_OPTIONS, MOCK_AUTOCOMPLETE_OPTIONS } from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('HeaderSearchAutocompleteItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState, mockGetters) => {
+ const store = new Vuex.Store({
+ state: {
+ loading: false,
+ ...initialState,
+ },
+ getters: {
+ autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ ...mockGetters,
+ },
+ });
+
+ wrapper = shallowMount(HeaderSearchAutocompleteItems, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
+ const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findGlAvatar = () => wrapper.findComponent(GlAvatar);
+
+ describe('template', () => {
+ describe('when loading is true', () => {
+ beforeEach(() => {
+ createComponent({ loading: true });
+ });
+
+ it('renders GlLoadingIcon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render autocomplete options', () => {
+ expect(findDropdownItems()).toHaveLength(0);
+ });
+ });
+
+ describe('when loading is false', () => {
+ beforeEach(() => {
+ createComponent({ loading: false });
+ });
+
+ it('does not render GlLoadingIcon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('Dropdown items', () => {
+ it('renders item for each option in autocomplete option', () => {
+ expect(findDropdownItems()).toHaveLength(MOCK_AUTOCOMPLETE_OPTIONS.length);
+ });
+
+ it('renders titles correctly', () => {
+ const expectedTitles = MOCK_AUTOCOMPLETE_OPTIONS.map((o) => o.label);
+ expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_AUTOCOMPLETE_OPTIONS.map((o) => o.url);
+ expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ });
+ });
+ describe.each`
+ item | showAvatar | avatarSize
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
+ ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)}
+ ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false}
+ `('GlAvatar', ({ item, showAvatar, avatarSize }) => {
+ describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => {
+ beforeEach(() => {
+ createComponent({}, { autocompleteGroupedSearchOptions: () => [item] });
+ });
+
+ it(`should${showAvatar ? '' : ' not'} render`, () => {
+ expect(findGlAvatar().exists()).toBe(showAvatar);
+ });
+
+ it(`should set avatarSize to ${avatarSize}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js
index 5963ad9c279..915b3a4a678 100644
--- a/spec/frontend/header_search/mock_data.js
+++ b/spec/frontend/header_search/mock_data.js
@@ -19,6 +19,8 @@ export const MOCK_MR_PATH = '/dashboard/merge_requests';
export const MOCK_ALL_PATH = '/';
+export const MOCK_AUTOCOMPLETE_PATH = '/autocomplete';
+
export const MOCK_PROJECT = {
id: 123,
name: 'MockProject',
@@ -81,3 +83,70 @@ export const MOCK_SCOPED_SEARCH_OPTIONS = [
url: MOCK_ALL_PATH,
},
];
+
+export const MOCK_AUTOCOMPLETE_OPTIONS = [
+ {
+ category: 'Projects',
+ id: 1,
+ label: 'MockProject1',
+ url: 'project/1',
+ },
+ {
+ category: 'Projects',
+ id: 2,
+ label: 'MockProject2',
+ url: 'project/2',
+ },
+ {
+ category: 'Groups',
+ id: 1,
+ label: 'MockGroup1',
+ url: 'group/1',
+ },
+ {
+ category: 'Help',
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
+ {
+ category: 'Projects',
+ data: [
+ {
+ category: 'Projects',
+ id: 1,
+ label: 'MockProject1',
+ url: 'project/1',
+ },
+ {
+ category: 'Projects',
+ id: 2,
+ label: 'MockProject2',
+ url: 'project/2',
+ },
+ ],
+ },
+ {
+ category: 'Groups',
+ data: [
+ {
+ category: 'Groups',
+ id: 1,
+ label: 'MockGroup1',
+ url: 'group/1',
+ },
+ ],
+ },
+ {
+ category: 'Help',
+ data: [
+ {
+ category: 'Help',
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+ ],
+ },
+];
diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js
index 4530df0d91c..ee2c72df77b 100644
--- a/spec/frontend/header_search/store/actions_spec.js
+++ b/spec/frontend/header_search/store/actions_spec.js
@@ -1,18 +1,50 @@
+import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import createFlash from '~/flash';
import * as actions from '~/header_search/store/actions';
import * as types from '~/header_search/store/mutation_types';
import createState from '~/header_search/store/state';
-import { MOCK_SEARCH } from '../mock_data';
+import axios from '~/lib/utils/axios_utils';
+import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS } from '../mock_data';
+
+jest.mock('~/flash');
describe('Header Search Store Actions', () => {
let state;
+ let mock;
+
+ const flashCallback = (callCount) => {
+ expect(createFlash).toHaveBeenCalledTimes(callCount);
+ createFlash.mockClear();
+ };
beforeEach(() => {
state = createState({});
+ mock = new MockAdapter(axios);
});
afterEach(() => {
state = null;
+ mock.restore();
+ });
+
+ describe.each`
+ axiosMock | type | expectedMutations | flashCallCount
+ ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS }]} | ${0}
+ ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]} | ${1}
+ `('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations, flashCallCount }) => {
+ describe(`on ${type}`, () => {
+ beforeEach(() => {
+ mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
+ });
+ it(`should dispatch the correct mutations`, () => {
+ return testAction({
+ action: actions.fetchAutocompleteOptions,
+ state,
+ expectedMutations,
+ }).then(() => flashCallback(flashCallCount));
+ });
+ });
});
describe('setSearch', () => {
diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js
index 2ad0a082f6a..d55db07188e 100644
--- a/spec/frontend/header_search/store/getters_spec.js
+++ b/spec/frontend/header_search/store/getters_spec.js
@@ -5,6 +5,7 @@ import {
MOCK_SEARCH_PATH,
MOCK_ISSUE_PATH,
MOCK_MR_PATH,
+ MOCK_AUTOCOMPLETE_PATH,
MOCK_SEARCH_CONTEXT,
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
@@ -12,6 +13,8 @@ import {
MOCK_GROUP,
MOCK_ALL_PATH,
MOCK_SEARCH,
+ MOCK_AUTOCOMPLETE_OPTIONS,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
} from '../mock_data';
describe('Header Search Store Getters', () => {
@@ -22,6 +25,7 @@ describe('Header Search Store Getters', () => {
searchPath: MOCK_SEARCH_PATH,
issuesPath: MOCK_ISSUE_PATH,
mrPath: MOCK_MR_PATH,
+ autocompletePath: MOCK_AUTOCOMPLETE_PATH,
searchContext: MOCK_SEARCH_CONTEXT,
...initialState,
});
@@ -56,6 +60,29 @@ describe('Header Search Store Getters', () => {
});
describe.each`
+ project | ref | expectedPath
+ ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=undefined&project_ref=null`}
+ ${MOCK_PROJECT} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=null`}
+ ${MOCK_PROJECT} | ${MOCK_PROJECT.id} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=${MOCK_PROJECT.id}`}
+ `('autocompleteQuery', ({ project, ref, expectedPath }) => {
+ describe(`when project is ${project?.name} and project ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ project,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.autocompleteQuery(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
group | group_metadata | project | project_metadata | expectedPath
${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH}
${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${'group/path'}
@@ -208,4 +235,17 @@ describe('Header Search Store Getters', () => {
);
});
});
+
+ describe('autocompleteGroupedSearchOptions', () => {
+ beforeEach(() => {
+ createState();
+ state.autocompleteOptions = MOCK_AUTOCOMPLETE_OPTIONS;
+ });
+
+ it('returns the correct grouped array', () => {
+ expect(getters.autocompleteGroupedSearchOptions(state)).toStrictEqual(
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ );
+ });
+ });
});
diff --git a/spec/frontend/header_search/store/mutations_spec.js b/spec/frontend/header_search/store/mutations_spec.js
index 8196c06099d..7f9b7631a7e 100644
--- a/spec/frontend/header_search/store/mutations_spec.js
+++ b/spec/frontend/header_search/store/mutations_spec.js
@@ -1,7 +1,7 @@
import * as types from '~/header_search/store/mutation_types';
import mutations from '~/header_search/store/mutations';
import createState from '~/header_search/store/state';
-import { MOCK_SEARCH } from '../mock_data';
+import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS } from '../mock_data';
describe('Header Search Store Mutations', () => {
let state;
@@ -10,6 +10,33 @@ describe('Header Search Store Mutations', () => {
state = createState({});
});
+ describe('REQUEST_AUTOCOMPLETE', () => {
+ it('sets loading to true and empties autocompleteOptions array', () => {
+ mutations[types.REQUEST_AUTOCOMPLETE](state);
+
+ expect(state.loading).toBe(true);
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ });
+ });
+
+ describe('RECEIVE_AUTOCOMPLETE_SUCCESS', () => {
+ it('sets loading to false and sets autocompleteOptions array', () => {
+ mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS);
+
+ expect(state.loading).toBe(false);
+ expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS);
+ });
+ });
+
+ describe('RECEIVE_AUTOCOMPLETE_ERROR', () => {
+ it('sets loading to false and empties autocompleteOptions array', () => {
+ mutations[types.RECEIVE_AUTOCOMPLETE_ERROR](state);
+
+ expect(state.loading).toBe(false);
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ });
+ });
+
describe('SET_SEARCH', () => {
it('sets search to value', () => {
mutations[types.SET_SEARCH](state, MOCK_SEARCH);
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
index 79ac0a8122a..3634599f328 100644
--- a/spec/frontend/ide/components/jobs/detail_spec.js
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -41,7 +41,7 @@ describe('IDE jobs detail view', () => {
});
it('scrolls to bottom', () => {
- expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled();
+ expect(vm.$refs.buildJobLog.scrollTo).toHaveBeenCalled();
});
it('renders job output', () => {
@@ -125,15 +125,15 @@ describe('IDE jobs detail view', () => {
beforeEach(() => {
vm = vm.$mount();
- jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation();
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollTo').mockImplementation();
});
it('scrolls build trace to bottom', () => {
- jest.spyOn(vm.$refs.buildTrace, 'scrollHeight', 'get').mockReturnValue(1000);
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollHeight', 'get').mockReturnValue(1000);
vm.scrollDown();
- expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000);
+ expect(vm.$refs.buildJobLog.scrollTo).toHaveBeenCalledWith(0, 1000);
});
});
@@ -141,26 +141,26 @@ describe('IDE jobs detail view', () => {
beforeEach(() => {
vm = vm.$mount();
- jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation();
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollTo').mockImplementation();
});
it('scrolls build trace to top', () => {
vm.scrollUp();
- expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0);
+ expect(vm.$refs.buildJobLog.scrollTo).toHaveBeenCalledWith(0, 0);
});
});
describe('scrollBuildLog', () => {
beforeEach(() => {
vm = vm.$mount();
- jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation();
- jest.spyOn(vm.$refs.buildTrace, 'offsetHeight', 'get').mockReturnValue(100);
- jest.spyOn(vm.$refs.buildTrace, 'scrollHeight', 'get').mockReturnValue(200);
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollTo').mockImplementation();
+ jest.spyOn(vm.$refs.buildJobLog, 'offsetHeight', 'get').mockReturnValue(100);
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollHeight', 'get').mockReturnValue(200);
});
it('sets scrollPos to bottom when at the bottom', () => {
- jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(100);
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollTop', 'get').mockReturnValue(100);
vm.scrollBuildLog();
@@ -168,7 +168,7 @@ describe('IDE jobs detail view', () => {
});
it('sets scrollPos to top when at the top', () => {
- jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(0);
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollTop', 'get').mockReturnValue(0);
vm.scrollPos = 1;
vm.scrollBuildLog();
@@ -177,7 +177,7 @@ describe('IDE jobs detail view', () => {
});
it('resets scrollPos when not at top or bottom', () => {
- jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(10);
+ jest.spyOn(vm.$refs.buildJobLog, 'scrollTop', 'get').mockReturnValue(10);
vm.scrollBuildLog();
diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js
index 7a07ed05201..1e34087b290 100644
--- a/spec/frontend/ide/stores/modules/commit/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js
@@ -126,7 +126,7 @@ describe('IDE commit module getters', () => {
);
expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe(
- 'Update test-file, index.js files',
+ 'Update test-file, index.js',
);
});
diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js
index 79b6b66319e..a8875e0cd02 100644
--- a/spec/frontend/ide/stores/utils_spec.js
+++ b/spec/frontend/ide/stores/utils_spec.js
@@ -94,7 +94,7 @@ describe('Multi-file store utils', () => {
{
action: commitActionTypes.move,
file_path: 'renamedFile',
- content: null,
+ content: undefined,
encoding: 'text',
last_commit_id: undefined,
previous_path: 'prevPath',
diff --git a/spec/frontend/import_entities/components/pagination_bar_spec.js b/spec/frontend/import_entities/components/pagination_bar_spec.js
new file mode 100644
index 00000000000..163ce11a8db
--- /dev/null
+++ b/spec/frontend/import_entities/components/pagination_bar_spec.js
@@ -0,0 +1,92 @@
+import { GlPagination, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import PaginationBar from '~/import_entities/components/pagination_bar.vue';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+
+describe('Pagination bar', () => {
+ const DEFAULT_PROPS = {
+ pageInfo: {
+ total: 50,
+ page: 1,
+ perPage: 20,
+ },
+ itemsCount: 17,
+ };
+ let wrapper;
+
+ const createComponent = (propsData) => {
+ wrapper = mount(PaginationBar, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...propsData,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('emits set-page event when page is selected', () => {
+ const NEXT_PAGE = 3;
+ // PaginationLinks uses prop instead of event for handling page change
+ // So we go one level deep to test this
+ wrapper
+ .findComponent(PaginationLinks)
+ .findComponent(GlPagination)
+ .vm.$emit('input', NEXT_PAGE);
+ expect(wrapper.emitted('set-page')).toEqual([[NEXT_PAGE]]);
+ });
+
+ it('emits set-page-size event when page size is selected', () => {
+ const firstItemInPageSizeDropdown = wrapper.findComponent(GlDropdownItem);
+ firstItemInPageSizeDropdown.vm.$emit('click');
+
+ const [emittedPageSizeChange] = wrapper.emitted('set-page-size')[0];
+ expect(firstItemInPageSizeDropdown.text()).toMatchInterpolatedText(
+ `${emittedPageSizeChange} items per page`,
+ );
+ });
+ });
+
+ it('renders current page size', () => {
+ const CURRENT_PAGE_SIZE = 40;
+
+ createComponent({
+ pageInfo: {
+ ...DEFAULT_PROPS.pageInfo,
+ perPage: CURRENT_PAGE_SIZE,
+ },
+ });
+
+ expect(wrapper.find(GlDropdown).find('button').text()).toMatchInterpolatedText(
+ `${CURRENT_PAGE_SIZE} items per page`,
+ );
+ });
+
+ it('renders current page information', () => {
+ createComponent();
+
+ expect(wrapper.find('[data-testid="information"]').text()).toMatchInterpolatedText(
+ 'Showing 1 - 17 of 50',
+ );
+ });
+
+ it('renders current page information when total count is over 1000', () => {
+ createComponent({
+ pageInfo: {
+ ...DEFAULT_PROPS.pageInfo,
+ total: 1200,
+ },
+ });
+
+ expect(wrapper.find('[data-testid="information"]').text()).toMatchInterpolatedText(
+ 'Showing 1 - 17 of 1000+',
+ );
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index ff602327592..0a9cbadb249 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -11,7 +11,7 @@ import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_field
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
-import { integrationLevels } from '~/integrations/edit/constants';
+import { integrationLevels } from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
describe('IntegrationForm', () => {
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 2860d3cc37a..119afbfecfe 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,6 +1,7 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { GET_JIRA_ISSUE_TYPES_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';
@@ -207,7 +208,7 @@ describe('JiraIssuesFields', () => {
await setEnableCheckbox(true);
await findJiraForVulnerabilities().vm.$emit('request-get-issue-types');
- expect(eventHubEmitSpy).toHaveBeenCalledWith('getJiraIssueTypes');
+ expect(eventHubEmitSpy).toHaveBeenCalledWith(GET_JIRA_ISSUE_TYPES_EVENT);
});
});
});
diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
index eb43d940f5e..90facaff1f9 100644
--- a/spec/frontend/integrations/edit/components/override_dropdown_spec.js
+++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
-import { integrationLevels, overrideDropdownDescriptions } from '~/integrations/edit/constants';
+import { integrationLevels, overrideDropdownDescriptions } from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
describe('OverrideDropdown', () => {
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
index cbb2ef380ba..f8f3f0fd318 100644
--- a/spec/frontend/integrations/integration_settings_form_spec.js
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -23,7 +23,7 @@ describe('IntegrationSettingsForm', () => {
it('should initialize form element refs on class object', () => {
// Form Reference
expect(integrationSettingsForm.$form).toBeDefined();
- expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
+ expect(integrationSettingsForm.$form.nodeName).toBe('FORM');
expect(integrationSettingsForm.formActive).toBeDefined();
});
@@ -43,14 +43,14 @@ describe('IntegrationSettingsForm', () => {
integrationSettingsForm.formActive = true;
integrationSettingsForm.toggleServiceState();
- expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
+ 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.attr('novalidate')).toBeDefined();
+ expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBeDefined();
});
});
@@ -67,8 +67,7 @@ describe('IntegrationSettingsForm', () => {
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
- // eslint-disable-next-line no-jquery/no-serialize
- formData = integrationSettingsForm.$form.serialize();
+ formData = new FormData(integrationSettingsForm.$form);
});
afterEach(() => {
@@ -145,8 +144,7 @@ describe('IntegrationSettingsForm', () => {
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
- // eslint-disable-next-line no-jquery/no-serialize
- formData = integrationSettingsForm.$form.serialize();
+ formData = new FormData(integrationSettingsForm.$form);
});
afterEach(() => {
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index dbed236d7df..ae89d05cead 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -1,16 +1,14 @@
-import { GlTable, GlLink, GlPagination } from '@gitlab/ui';
+import { GlTable, GlLink, GlPagination, GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_PER_PAGE } from '~/api';
-import createFlash from '~/flash';
import IntegrationOverrides from '~/integrations/overrides/components/integration_overrides.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
-jest.mock('~/flash');
-
const mockOverrides = Array(DEFAULT_PER_PAGE * 3)
.fill(1)
.map((_, index) => ({
@@ -62,6 +60,7 @@ describe('IntegrationOverrides', () => {
text: link.text(),
};
});
+ const findAlert = () => wrapper.findComponent(GlAlert);
describe('while loading', () => {
it('sets GlTable `busy` attribute to `true`', () => {
@@ -104,18 +103,26 @@ describe('IntegrationOverrides', () => {
describe('when request fails', () => {
beforeEach(async () => {
+ jest.spyOn(Sentry, 'captureException');
mockAxios.onGet(defaultProps.overridesPath).reply(httpStatus.INTERNAL_SERVER_ERROR);
+
createComponent();
await waitForPromises();
});
- it('calls createFlash', () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
- message: IntegrationOverrides.i18n.defaultErrorMessage,
- captureError: true,
- error: expect.any(Error),
- });
+ it('displays error alert', () => {
+ const alert = findAlert();
+ expect(alert.exists()).toBe(true);
+ expect(alert.text()).toBe(IntegrationOverrides.i18n.defaultErrorMessage);
+ });
+
+ it('hides overrides table', () => {
+ const table = findGlTable();
+ expect(table.exists()).toBe(false);
+ });
+
+ it('captures exception in Sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
});
});
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 95b1c55b82d..8c3c549a5eb 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -242,7 +242,7 @@ describe('InviteMembersModal', () => {
};
const expectedEmailRestrictedError =
- "email 'email@example.com' does not match the allowed domains: example1.org";
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
const expectedSyntaxError = 'email contains an invalid email address';
it('calls the API with the expected focus data when an areas_of_focus checkbox is clicked', () => {
@@ -421,7 +421,7 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe(
- "root: User email 'admin@example.com' does not match the allowed domain of example2.com",
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
);
expect(findMembersSelect().props('validationState')).toBe(false);
});
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
index 79b56a33708..dd84b4fd78f 100644
--- a/spec/frontend/invite_members/mock_data/api_responses.js
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -9,7 +9,7 @@ const INVITATIONS_API_ERROR_EMAIL_INVALID = {
const INVITATIONS_API_EMAIL_RESTRICTED = {
message: {
'email@example.com':
- "Invite email 'email@example.com' does not match the allowed domains: example1.org",
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
},
status: 'error',
};
@@ -17,9 +17,9 @@ const INVITATIONS_API_EMAIL_RESTRICTED = {
const INVITATIONS_API_MULTIPLE_EMAIL_RESTRICTED = {
message: {
'email@example.com':
- "Invite email email 'email@example.com' does not match the allowed domains: example1.org",
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
'email4@example.com':
- "Invite email email 'email4@example.com' does not match the allowed domains: example1.org",
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
},
status: 'error',
};
@@ -36,7 +36,11 @@ const MEMBERS_API_MEMBER_ALREADY_EXISTS = {
};
const MEMBERS_API_SINGLE_USER_RESTRICTED = {
- message: { user: ["email 'email@example.com' does not match the allowed domains: example1.org"] },
+ message: {
+ user: [
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
+ ],
+ },
};
const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = {
@@ -49,7 +53,7 @@ const MEMBERS_API_SINGLE_USER_ACCESS_LEVEL = {
const MEMBERS_API_MULTIPLE_USERS_RESTRICTED = {
message:
- "root: User email 'admin@example.com' does not match the allowed domain of example2.com and user18: User email 'user18@example.org' does not match the allowed domain of example2.com",
+ "root: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups. and user18: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist. and john_doe31: The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Email restrictions for sign-ups.",
status: 'error',
};
diff --git a/spec/frontend/invite_members/utils/response_message_parser_spec.js b/spec/frontend/invite_members/utils/response_message_parser_spec.js
index 3c88b5a2418..e2cc87c8547 100644
--- a/spec/frontend/invite_members/utils/response_message_parser_spec.js
+++ b/spec/frontend/invite_members/utils/response_message_parser_spec.js
@@ -2,18 +2,20 @@ import {
responseMessageFromSuccess,
responseMessageFromError,
} from '~/invite_members/utils/response_message_parser';
+import { membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
describe('Response message parser', () => {
- const expectedMessage = 'expected display message';
+ const expectedMessage = 'expected display and message.';
describe('parse message from successful response', () => {
const exampleKeyedMsg = { 'email@example.com': expectedMessage };
+ const exampleFirstPartMultiple = 'username1: expected display and message.';
const exampleUserMsgMultiple =
- ' and username1: id not found and username2: email is restricted';
+ ' and username2: id not found and restricted email. and username3: email is restricted.';
it.each([
[[{ data: { message: expectedMessage } }]],
- [[{ data: { message: expectedMessage + exampleUserMsgMultiple } }]],
+ [[{ data: { message: exampleFirstPartMultiple + exampleUserMsgMultiple } }]],
[[{ data: { error: expectedMessage } }]],
[[{ data: { message: [expectedMessage] } }]],
[[{ data: { message: exampleKeyedMsg } }]],
@@ -33,4 +35,24 @@ describe('Response message parser', () => {
expect(responseMessageFromError(errorResponse)).toBe(expectedMessage);
});
});
+
+ describe('displaying only the first error when a response has messages for multiple users', () => {
+ const expected =
+ "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
+
+ it.each([
+ [[{ data: membersApiResponse.MULTIPLE_USERS_RESTRICTED }]],
+ [[{ data: invitationsApiResponse.MULTIPLE_EMAIL_RESTRICTED }]],
+ [[{ data: invitationsApiResponse.EMAIL_RESTRICTED }]],
+ ])(`returns "${expectedMessage}" from success response: %j`, (restrictedResponse) => {
+ expect(responseMessageFromSuccess(restrictedResponse)).toBe(expected);
+ });
+
+ it.each([[{ response: { data: membersApiResponse.SINGLE_USER_RESTRICTED } }]])(
+ `returns "${expectedMessage}" from error response: %j`,
+ (singleRestrictedResponse) => {
+ expect(responseMessageFromError(singleRestrictedResponse)).toBe(expected);
+ },
+ );
+ });
});
diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js
index 34094d22e68..ad4abda6912 100644
--- a/spec/frontend/issuable/components/csv_export_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_export_modal_spec.js
@@ -61,11 +61,6 @@ describe('CsvExportModal', () => {
expect(wrapper.text()).toContain('10 issues selected');
expect(findIcon().exists()).toBe(true);
});
-
- it("doesn't display the info text when issuableCount is -1", () => {
- wrapper = createComponent({ props: { issuableCount: -1 } });
- expect(wrapper.text()).not.toContain('issues selected');
- });
});
describe('email info text', () => {
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index 0c88b6b1283..307323ef07a 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -17,7 +17,6 @@ describe('CsvImportModal', () => {
...props,
},
provide: {
- issuableType: 'issues',
...injectedProperties,
},
stubs: {
@@ -43,9 +42,9 @@ describe('CsvImportModal', () => {
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
describe('template', () => {
- it('displays modal title', () => {
+ it('passes correct title props to modal', () => {
wrapper = createComponent();
- expect(findModal().text()).toContain('Import issues');
+ expect(findModal().props('title')).toContain('Import issues');
});
it('displays a note about the maximum allowed file size', () => {
@@ -73,7 +72,7 @@ describe('CsvImportModal', () => {
});
it('submits the form when the primary action is clicked', () => {
- findPrimaryButton().trigger('click');
+ findModal().vm.$emit('primary');
expect(formSubmitSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index 173d12757e3..ff6922989cb 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -1,5 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
+import IssueToken from '~/related_issues/components/issue_token.vue';
import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
const issuable1 = {
@@ -22,7 +23,7 @@ const issuable2 = {
const pathIdSeparator = PathIdSeparator.Issue;
-const findFormInput = (wrapper) => wrapper.find('.js-add-issuable-form-input').element;
+const findFormInput = (wrapper) => wrapper.find('input').element;
const findRadioInput = (inputs, value) =>
inputs.filter((input) => input.element.value === value)[0];
@@ -105,11 +106,11 @@ describe('AddIssuableForm', () => {
});
it('should put input value in place', () => {
- expect(findFormInput(wrapper).value).toEqual(inputValue);
+ expect(findFormInput(wrapper).value).toBe(inputValue);
});
it('should render pending issuables items', () => {
- expect(wrapper.findAll('.js-add-issuable-form-token-list-item').length).toEqual(2);
+ expect(wrapper.findAllComponents(IssueToken)).toHaveLength(2);
});
it('should not have disabled submit button', () => {
diff --git a/spec/frontend/issuable_form_spec.js b/spec/frontend/issuable_form_spec.js
index bc7a87eb65c..c77fde4261e 100644
--- a/spec/frontend/issuable_form_spec.js
+++ b/spec/frontend/issuable_form_spec.js
@@ -20,16 +20,13 @@ describe('IssuableForm', () => {
describe('removeWip', () => {
it.each`
prefix
- ${'drAft '}
${'draFT: '}
${' [DRaft] '}
${'drAft:'}
${'[draFT]'}
- ${' dRaFt - '}
- ${'dRaFt - '}
${'(draft) '}
${' (DrafT)'}
- ${'draft draft - draft: [draft] (draft)'}
+ ${'draft: [draft] (draft)'}
`('removes "$prefix" from the beginning of the title', ({ prefix }) => {
instance.titleField.val(`${prefix}The Issuable's Title Value`);
@@ -48,4 +45,18 @@ describe('IssuableForm', () => {
expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
});
});
+
+ describe('workInProgress', () => {
+ it.each`
+ title | expected
+ ${'draFT: something is happening'} | ${true}
+ ${'draft something is happening'} | ${false}
+ ${'something is happening to drafts'} | ${false}
+ ${'something is happening'} | ${false}
+ `('returns $expected with "$title"', ({ title, expected }) => {
+ instance.titleField.val(title);
+
+ expect(instance.workInProgress()).toBe(expected);
+ });
+ });
});
diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js
index ea36d59ff83..ac3bf7f3269 100644
--- a/spec/frontend/issuable_list/components/issuable_item_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_item_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlLabel, GlIcon, GlFormCheckbox } from '@gitlab/ui';
+import { GlLink, GlLabel, GlIcon, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import IssuableItem from '~/issuable_list/components/issuable_item.vue';
@@ -16,6 +16,9 @@ const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots
showCheckbox: false,
},
slots,
+ stubs: {
+ GlSprintf,
+ },
});
const MOCK_GITLAB_URL = 'http://0.0.0.0:3000';
@@ -135,13 +138,6 @@ describe('IssuableItem', () => {
});
});
- describe('createdAt', () => {
- it('returns string containing timeago string based on `issuable.createdAt`', () => {
- expect(wrapper.vm.createdAt).toContain('created');
- expect(wrapper.vm.createdAt).toContain('ago');
- });
- });
-
describe('updatedAt', () => {
it('returns string containing timeago string based on `issuable.updatedAt`', () => {
expect(wrapper.vm.updatedAt).toContain('updated');
@@ -449,8 +445,7 @@ describe('IssuableItem', () => {
it('renders issuable updatedAt info', () => {
const updatedAtEl = wrapper.find('[data-testid="issuable-updated-at"]');
- expect(updatedAtEl.exists()).toBe(true);
- expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am UTC');
+ expect(updatedAtEl.attributes('title')).toBe('Sep 10, 2020 11:41am UTC');
expect(updatedAtEl.text()).toBe(wrapper.vm.updatedAt);
});
diff --git a/spec/frontend/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index 39083b3d8fb..45f96103e3e 100644
--- a/spec/frontend/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
@@ -6,10 +6,10 @@ import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_ima
import mockData from '../mock_data';
describe('Issuable suggestions suggestion component', () => {
- let vm;
+ let wrapper;
function createComponent(suggestion = {}) {
- vm = shallowMount(Suggestion, {
+ wrapper = shallowMount(Suggestion, {
propsData: {
suggestion: {
...mockData(),
@@ -19,37 +19,40 @@ describe('Issuable suggestions suggestion component', () => {
});
}
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findAuthorLink = () => wrapper.findAll(GlLink).at(1);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
+ const findUserAvatar = () => wrapper.findComponent(UserAvatarImage);
+
afterEach(() => {
- vm.destroy();
+ wrapper.destroy();
});
it('renders title', () => {
createComponent();
- expect(vm.text()).toContain('Test issue');
+ expect(wrapper.text()).toContain('Test issue');
});
it('renders issue link', () => {
createComponent();
- const link = vm.find(GlLink);
-
- expect(link.attributes('href')).toBe(`${TEST_HOST}/test/issue/1`);
+ expect(findLink().attributes('href')).toBe(`${TEST_HOST}/test/issue/1`);
});
it('renders IID', () => {
createComponent();
- expect(vm.text()).toContain('#1');
+ expect(wrapper.text()).toContain('#1');
});
describe('opened state', () => {
it('renders icon', () => {
createComponent();
- const icon = vm.find(GlIcon);
-
- expect(icon.props('name')).toBe('issue-open-m');
+ expect(findIcon().props('name')).toBe('issue-open-m');
+ expect(findIcon().attributes('class')).toMatch('gl-text-green-500');
});
it('renders created timeago', () => {
@@ -57,10 +60,8 @@ describe('Issuable suggestions suggestion component', () => {
closedAt: '',
});
- const tooltip = vm.find(GlTooltip);
-
- expect(tooltip.find('.d-block').text()).toContain('Opened');
- expect(tooltip.text()).toContain('3 days ago');
+ expect(findTooltip().text()).toContain('Opened');
+ expect(findTooltip().text()).toContain('3 days ago');
});
});
@@ -70,18 +71,15 @@ describe('Issuable suggestions suggestion component', () => {
state: 'closed',
});
- const icon = vm.find(GlIcon);
-
- expect(icon.props('name')).toBe('issue-close');
+ expect(findIcon().props('name')).toBe('issue-close');
+ expect(findIcon().attributes('class')).toMatch('gl-text-blue-500');
});
it('renders closed timeago', () => {
createComponent();
- const tooltip = vm.find(GlTooltip);
-
- expect(tooltip.find('.d-block').text()).toContain('Opened');
- expect(tooltip.text()).toContain('1 day ago');
+ expect(findTooltip().text()).toContain('Opened');
+ expect(findTooltip().text()).toContain('1 day ago');
});
});
@@ -89,18 +87,14 @@ describe('Issuable suggestions suggestion component', () => {
it('renders author info', () => {
createComponent();
- const link = vm.findAll(GlLink).at(1);
-
- expect(link.text()).toContain('Author Name');
- expect(link.text()).toContain('@author.username');
+ expect(findAuthorLink().text()).toContain('Author Name');
+ expect(findAuthorLink().text()).toContain('@author.username');
});
it('renders author image', () => {
createComponent();
- const image = vm.find(UserAvatarImage);
-
- expect(image.props('imgSrc')).toBe(`${TEST_HOST}/avatar`);
+ expect(findUserAvatar().props('imgSrc')).toBe(`${TEST_HOST}/avatar`);
});
});
@@ -108,7 +102,7 @@ describe('Issuable suggestions suggestion component', () => {
it('renders upvotes count', () => {
createComponent();
- const count = vm.findAll('.suggestion-counts span').at(0);
+ const count = wrapper.findAll('.suggestion-counts span').at(0);
expect(count.text()).toContain('1');
expect(count.find(GlIcon).props('name')).toBe('thumb-up');
@@ -117,7 +111,7 @@ describe('Issuable suggestions suggestion component', () => {
it('renders notes count', () => {
createComponent();
- const count = vm.findAll('.suggestion-counts span').at(1);
+ const count = wrapper.findAll('.suggestion-counts span').at(1);
expect(count.text()).toContain('2');
expect(count.find(GlIcon).props('name')).toBe('comment');
@@ -130,10 +124,9 @@ describe('Issuable suggestions suggestion component', () => {
confidential: true,
});
- const icon = vm.find(GlIcon);
-
- expect(icon.props('name')).toBe('eye-slash');
- expect(icon.attributes('title')).toBe('Confidential');
+ expect(findIcon().props('name')).toBe('eye-slash');
+ expect(findIcon().attributes('class')).toMatch('gl-text-orange-500');
+ expect(findIcon().attributes('title')).toBe('Confidential');
});
});
});
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 8d79a5eed35..6b443062f12 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -24,6 +24,7 @@ import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
+import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue';
import {
CREATED_DESC,
DUE_DATE_OVERDUE,
@@ -65,6 +66,7 @@ describe('IssuesListApp component', () => {
exportCsvPath: 'export/csv/path',
fullPath: 'path/to/project',
hasAnyIssues: true,
+ hasAnyProjects: true,
hasBlockedIssuesFeature: true,
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
@@ -93,6 +95,7 @@ describe('IssuesListApp component', () => {
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
const findGlLink = () => wrapper.findComponent(GlLink);
const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
const mountComponent = ({
provide = {},
@@ -190,10 +193,7 @@ describe('IssuesListApp component', () => {
beforeEach(() => {
setWindowLocation(search);
- wrapper = mountComponent({
- provide: { isSignedIn: true },
- mountFn: mount,
- });
+ wrapper = mountComponent({ provide: { isSignedIn: true }, mountFn: mount });
jest.runOnlyPendingTimers();
});
@@ -208,7 +208,7 @@ describe('IssuesListApp component', () => {
describe('when user is not signed in', () => {
it('does not render', () => {
- wrapper = mountComponent({ provide: { isSignedIn: false } });
+ wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount });
expect(findCsvImportExportButtons().exists()).toBe(false);
});
@@ -216,7 +216,7 @@ describe('IssuesListApp component', () => {
describe('when in a group context', () => {
it('does not render', () => {
- wrapper = mountComponent({ provide: { isProject: false } });
+ wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount });
expect(findCsvImportExportButtons().exists()).toBe(false);
});
@@ -231,7 +231,7 @@ describe('IssuesListApp component', () => {
});
it('does not render when user does not have permissions', () => {
- wrapper = mountComponent({ provide: { canBulkUpdate: false } });
+ wrapper = mountComponent({ provide: { canBulkUpdate: false }, mountFn: mount });
expect(findGlButtons().filter((button) => button.text() === 'Edit issues')).toHaveLength(0);
});
@@ -258,11 +258,25 @@ describe('IssuesListApp component', () => {
});
it('does not render when user does not have permissions', () => {
- wrapper = mountComponent({ provide: { showNewIssueLink: false } });
+ wrapper = mountComponent({ provide: { showNewIssueLink: false }, mountFn: mount });
expect(findGlButtons().filter((button) => button.text() === 'New issue')).toHaveLength(0);
});
});
+
+ describe('new issue split dropdown', () => {
+ it('does not render in a project context', () => {
+ wrapper = mountComponent({ provide: { isProject: true }, mountFn: mount });
+
+ expect(findNewIssueDropdown().exists()).toBe(false);
+ });
+
+ it('renders in a group context', () => {
+ wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount });
+
+ expect(findNewIssueDropdown().exists()).toBe(true);
+ });
+ });
});
describe('initial url params', () => {
@@ -506,7 +520,7 @@ describe('IssuesListApp component', () => {
beforeEach(() => {
wrapper = mountComponent({
provide: {
- groupEpicsPath: '',
+ groupPath: '',
},
});
});
@@ -522,7 +536,7 @@ describe('IssuesListApp component', () => {
beforeEach(() => {
wrapper = mountComponent({
provide: {
- groupEpicsPath: '',
+ groupPath: '',
},
});
});
@@ -550,7 +564,7 @@ describe('IssuesListApp component', () => {
provide: {
isSignedIn: true,
projectIterationsPath: 'project/iterations/path',
- groupEpicsPath: 'group/epics/path',
+ groupPath: 'group/path',
hasIssueWeightsFeature: true,
},
});
diff --git a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
new file mode 100644
index 00000000000..1fcaa99cf5a
--- /dev/null
+++ b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
@@ -0,0 +1,131 @@
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue';
+import searchProjectsQuery from '~/issues_list/queries/search_projects.query.graphql';
+import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
+import {
+ emptySearchProjectsQueryResponse,
+ project1,
+ project2,
+ searchProjectsQueryResponse,
+} from '../mock_data';
+
+describe('NewIssueDropdown component', () => {
+ let wrapper;
+
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const mountComponent = ({
+ search = '',
+ queryResponse = searchProjectsQueryResponse,
+ mountFn = shallowMount,
+ } = {}) => {
+ const requestHandlers = [[searchProjectsQuery, jest.fn().mockResolvedValue(queryResponse)]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return mountFn(NewIssueDropdown, {
+ localVue,
+ apolloProvider,
+ provide: {
+ fullPath: 'mushroom-kingdom',
+ },
+ data() {
+ return { search };
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('shown');
+ await wrapper.vm.$apollo.queries.projects.refetch();
+ jest.runOnlyPendingTimers();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a split dropdown', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().props('split')).toBe(true);
+ });
+
+ it('renders a label for the dropdown toggle button', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().attributes('toggle-text')).toBe(NewIssueDropdown.i18n.toggleButtonLabel);
+ });
+
+ it('focuses on input when dropdown is shown', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
+
+ await showDropdown();
+
+ expect(inputSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders expected dropdown items', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ await showDropdown();
+
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
+ expect(listItems.at(1).text()).toBe(project2.nameWithNamespace);
+ });
+
+ it('renders `No matches found` when there are no matches', async () => {
+ wrapper = mountComponent({
+ search: 'no matches',
+ queryResponse: emptySearchProjectsQueryResponse,
+ mountFn: mount,
+ });
+
+ await showDropdown();
+
+ expect(wrapper.find('li').text()).toBe(NewIssueDropdown.i18n.noMatchesFound);
+ });
+
+ describe('when no project is selected', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ it('dropdown button is not a link', () => {
+ expect(findDropdown().attributes('split-href')).toBeUndefined();
+ });
+
+ it('displays default text on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(NewIssueDropdown.i18n.defaultDropdownText);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ await showDropdown();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ });
+
+ it('dropdown button is a link', () => {
+ const href = joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new');
+
+ expect(findDropdown().attributes('split-href')).toBe(href);
+ });
+
+ it('displays project name on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(`New issue in ${project1.name}`);
+ });
+ });
+});
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index 720f9cac986..3be256d8094 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -221,3 +221,37 @@ export const urlParamsWithSpecialValues = {
epic_id: 'None',
weight: 'None',
};
+
+export const project1 = {
+ id: 'gid://gitlab/Group/26',
+ name: 'Super Mario Project',
+ nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
+};
+
+export const project2 = {
+ id: 'gid://gitlab/Group/59',
+ name: 'Mario Kart Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
+};
+
+export const searchProjectsQueryResponse = {
+ data: {
+ group: {
+ projects: {
+ nodes: [project1, project2],
+ },
+ },
+ },
+};
+
+export const emptySearchProjectsQueryResponse = {
+ data: {
+ group: {
+ projects: {
+ nodes: [],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index 891ba9c223c..9f5b772a5c7 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -127,21 +127,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
>
<!---->
- <div
- class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5"
- >
- <div
- class="gl-display-flex"
- >
- <!---->
- </div>
-
- <div
- class="gl-display-flex"
- >
- <!---->
- </div>
- </div>
+ <!---->
<div
class="gl-new-dropdown-contents"
@@ -272,21 +258,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
>
<!---->
- <div
- class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5"
- >
- <div
- class="gl-display-flex"
- >
- <!---->
- </div>
-
- <div
- class="gl-display-flex"
- >
- <!---->
- </div>
- </div>
+ <!---->
<div
class="gl-new-dropdown-contents"
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index f8a0059bf21..07e6ee46c41 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -2,7 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import { TEST_HOST } from 'helpers/test_constants';
import EmptyState from '~/jobs/components/empty_state.vue';
import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
@@ -19,8 +19,6 @@ describe('Job App', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
-
let store;
let wrapper;
let mock;
@@ -47,9 +45,9 @@ describe('Job App', () => {
wrapper = mount(JobApp, { propsData: { ...props }, store });
};
- const setupAndMount = ({ jobData = {}, traceData = {} } = {}) => {
+ const setupAndMount = ({ jobData = {}, jobLogData = {} } = {}) => {
mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData });
- mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, traceData);
+ mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, jobLogData);
const asyncInit = store.dispatch('init', initSettings);
@@ -77,11 +75,10 @@ describe('Job App', () => {
const findEmptyState = () => wrapper.find(EmptyState);
const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]');
const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
- const findJobTraceScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]');
- const findJobTraceScrollBottom = () =>
- wrapper.find('[data-testid="job-controller-scroll-bottom"]');
- const findJobTraceController = () => wrapper.find('[data-testid="job-raw-link-controller"]');
- const findJobTraceEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]');
+ const findJobLogScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]');
+ const findJobLogScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]');
+ const findJobLogController = () => wrapper.find('[data-testid="job-raw-link-controller"]');
+ const findJobLogEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]');
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -315,7 +312,7 @@ describe('Job App', () => {
});
describe('empty states block', () => {
- it('renders empty state when job does not have trace and is not running', () =>
+ it('renders empty state when job does not have log and is not running', () =>
setupAndMount({
jobData: {
has_trace: false,
@@ -342,7 +339,7 @@ describe('Job App', () => {
expect(findEmptyState().exists()).toBe(true);
}));
- it('does not render empty state when job does not have trace but it is running', () =>
+ it('does not render empty state when job does not have log but it is running', () =>
setupAndMount({
jobData: {
has_trace: false,
@@ -358,7 +355,7 @@ describe('Job App', () => {
expect(findEmptyState().exists()).toBe(false);
}));
- it('does not render empty state when job has trace but it is not running', () =>
+ it('does not render empty state when job has log but it is not running', () =>
setupAndMount({ jobData: { has_trace: true } }).then(() => {
expect(findEmptyState().exists()).toBe(false);
}));
@@ -424,10 +421,10 @@ describe('Job App', () => {
});
});
- describe('trace controls', () => {
+ describe('job log controls', () => {
beforeEach(() =>
setupAndMount({
- traceData: {
+ jobLogData: {
html: '<span>Update</span>',
status: 'success',
append: false,
@@ -439,16 +436,16 @@ describe('Job App', () => {
);
it('should render scroll buttons', () => {
- expect(findJobTraceScrollTop().exists()).toBe(true);
- expect(findJobTraceScrollBottom().exists()).toBe(true);
+ expect(findJobLogScrollTop().exists()).toBe(true);
+ expect(findJobLogScrollBottom().exists()).toBe(true);
});
it('should render link to raw ouput', () => {
- expect(findJobTraceController().exists()).toBe(true);
+ expect(findJobLogController().exists()).toBe(true);
});
it('should render link to erase job', () => {
- expect(findJobTraceEraseLink().exists()).toBe(true);
+ expect(findJobLogEraseLink().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job_container_item_spec.js
index 36038b69e64..6b488821bc1 100644
--- a/spec/frontend/jobs/components/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job_container_item_spec.js
@@ -1,12 +1,12 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import JobContainerItem from '~/jobs/components/job_container_item.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import job from '../mock_data';
describe('JobContainerItem', () => {
let wrapper;
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const findCiIconComponent = () => wrapper.findComponent(CiIcon);
const findGlIconComponent = () => wrapper.findComponent(GlIcon);
diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js
index 97b0333cb32..0ba07522243 100644
--- a/spec/frontend/jobs/components/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job_log_controllers_spec.js
@@ -18,7 +18,7 @@ describe('Job log controllers', () => {
isScrollTopDisabled: false,
isScrollBottomDisabled: false,
isScrollingDown: true,
- isTraceSizeVisible: true,
+ isJobLogSizeVisible: true,
};
const createWrapper = (props) => {
@@ -38,7 +38,7 @@ describe('Job log controllers', () => {
const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]');
describe('Truncate information', () => {
- describe('with isTraceSizeVisible', () => {
+ describe('with isJobLogSizeVisible', () => {
beforeEach(() => {
createWrapper();
});
@@ -47,31 +47,31 @@ describe('Job log controllers', () => {
expect(findTruncatedInfo().text()).toMatch('499.95 KiB');
});
- it('renders link to raw trace', () => {
+ it('renders link to raw job log', () => {
expect(findRawLink().attributes('href')).toBe(defaultProps.rawPath);
});
});
});
describe('links section', () => {
- describe('with raw trace path', () => {
+ describe('with raw job log path', () => {
beforeEach(() => {
createWrapper();
});
- it('renders raw trace link', () => {
+ it('renders raw job log link', () => {
expect(findRawLinkController().attributes('href')).toBe(defaultProps.rawPath);
});
});
- describe('without raw trace path', () => {
+ describe('without raw job log path', () => {
beforeEach(() => {
createWrapper({
rawPath: null,
});
});
- it('does not render raw trace link', () => {
+ it('does not render raw job log link', () => {
expect(findRawLinkController().exists()).toBe(false);
});
});
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index 4e23a3ba7b8..96bdf03796b 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -6,7 +6,7 @@ describe('Job Log Collapsible Section', () => {
let wrapper;
let origGon;
- const traceEndpoint = 'jobs/335';
+ const jobLogEndpoint = 'jobs/335';
const findCollapsibleLine = () => wrapper.find('.collapsible-line');
const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg');
@@ -35,7 +35,7 @@ describe('Job Log Collapsible Section', () => {
beforeEach(() => {
createComponent({
section: collapsibleSectionClosed,
- traceEndpoint,
+ jobLogEndpoint,
});
});
@@ -52,7 +52,7 @@ describe('Job Log Collapsible Section', () => {
beforeEach(() => {
createComponent({
section: collapsibleSectionOpened,
- traceEndpoint,
+ jobLogEndpoint,
});
});
@@ -72,7 +72,7 @@ describe('Job Log Collapsible Section', () => {
it('emits onClickCollapsibleLine on click', () => {
createComponent({
section: collapsibleSectionOpened,
- traceEndpoint,
+ jobLogEndpoint,
});
findCollapsibleLine().trigger('click');
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index 99fb6846ce5..9a5522ab4cd 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -31,8 +31,8 @@ describe('Job Log', () => {
window.gon = { features: { infinitelyCollapsibleSections: false } };
state = {
- trace: logLinesParserLegacy(jobLog),
- traceEndpoint: 'jobs/id',
+ jobLog: logLinesParserLegacy(jobLog),
+ jobLogEndpoint: 'jobs/id',
};
store = new Vuex.Store({
@@ -59,7 +59,7 @@ describe('Job Log', () => {
});
it('links to the provided path and correct line number', () => {
- expect(wrapper.find('#L1').attributes('href')).toBe(`${state.traceEndpoint}#L1`);
+ expect(wrapper.find('#L1').attributes('href')).toBe(`${state.jobLogEndpoint}#L1`);
});
});
@@ -111,8 +111,8 @@ describe('Job Log, infinitelyCollapsibleSections feature flag enabled', () => {
window.gon = { features: { infinitelyCollapsibleSections: true } };
state = {
- trace: logLinesParser(jobLog).parsedLines,
- traceEndpoint: 'jobs/id',
+ jobLog: logLinesParser(jobLog).parsedLines,
+ jobLogEndpoint: 'jobs/id',
};
store = new Vuex.Store({
@@ -139,7 +139,7 @@ describe('Job Log, infinitelyCollapsibleSections feature flag enabled', () => {
});
it('links to the provided path and correct line number', () => {
- expect(wrapper.find('#L1').attributes('href')).toBe(`${state.traceEndpoint}#L1`);
+ expect(wrapper.find('#L1').attributes('href')).toBe(`${state.jobLogEndpoint}#L1`);
});
});
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
index 838323df755..63dcd72f967 100644
--- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
+++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
+import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
describe('DelayedJobMixin', () => {
let wrapper;
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const dummyComponent = {
props: {
job: {
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js
index a29bd15099f..16448d6a3ca 100644
--- a/spec/frontend/jobs/store/actions_spec.js
+++ b/spec/frontend/jobs/store/actions_spec.js
@@ -3,7 +3,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import {
setJobEndpoint,
- setTraceOptions,
+ setJobLogOptions,
clearEtagPoll,
stopPolling,
requestJob,
@@ -12,12 +12,12 @@ import {
receiveJobError,
scrollTop,
scrollBottom,
- requestTrace,
- fetchTrace,
- startPollingTrace,
- stopPollingTrace,
- receiveTraceSuccess,
- receiveTraceError,
+ requestJobLog,
+ fetchJobLog,
+ startPollingJobLog,
+ stopPollingJobLog,
+ receiveJobLogSuccess,
+ receiveJobLogError,
toggleCollapsibleLine,
requestJobsForStage,
fetchJobsForStage,
@@ -51,13 +51,13 @@ describe('Job State actions', () => {
});
});
- describe('setTraceOptions', () => {
- it('should commit SET_TRACE_OPTIONS mutation', (done) => {
+ describe('setJobLogOptions', () => {
+ it('should commit SET_JOB_LOG_OPTIONS mutation', (done) => {
testAction(
- setTraceOptions,
+ setJobLogOptions,
{ pagePath: 'job/872324/trace.json' },
mockedState,
- [{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
+ [{ type: types.SET_JOB_LOG_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
[],
done,
);
@@ -191,17 +191,17 @@ describe('Job State actions', () => {
});
});
- describe('requestTrace', () => {
- it('should commit REQUEST_TRACE mutation', (done) => {
- testAction(requestTrace, null, mockedState, [{ type: types.REQUEST_TRACE }], [], done);
+ describe('requestJobLog', () => {
+ it('should commit REQUEST_JOB_LOG mutation', (done) => {
+ testAction(requestJobLog, null, mockedState, [{ type: types.REQUEST_JOB_LOG }], [], done);
});
});
- describe('fetchTrace', () => {
+ describe('fetchJobLog', () => {
let mock;
beforeEach(() => {
- mockedState.traceEndpoint = `${TEST_HOST}/endpoint`;
+ mockedState.jobLogEndpoint = `${TEST_HOST}/endpoint`;
mock = new MockAdapter(axios);
});
@@ -212,14 +212,14 @@ describe('Job State actions', () => {
});
describe('success', () => {
- it('dispatches requestTrace, receiveTraceSuccess and stopPollingTrace when job is complete', (done) => {
+ it('dispatches requestJobLog, receiveJobLogSuccess and stopPollingJobLog when job is complete', (done) => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
});
testAction(
- fetchTrace,
+ fetchJobLog,
null,
mockedState,
[],
@@ -233,10 +233,10 @@ describe('Job State actions', () => {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
},
- type: 'receiveTraceSuccess',
+ type: 'receiveJobLogSuccess',
},
{
- type: 'stopPollingTrace',
+ type: 'stopPollingJobLog',
},
],
done,
@@ -244,43 +244,43 @@ describe('Job State actions', () => {
});
describe('when job is incomplete', () => {
- let tracePayload;
+ let jobLogPayload;
beforeEach(() => {
- tracePayload = {
+ jobLogPayload = {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: false,
};
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, tracePayload);
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, jobLogPayload);
});
- it('dispatches startPollingTrace', (done) => {
+ it('dispatches startPollingJobLog', (done) => {
testAction(
- fetchTrace,
+ fetchJobLog,
null,
mockedState,
[],
[
{ type: 'toggleScrollisInBottom', payload: true },
- { type: 'receiveTraceSuccess', payload: tracePayload },
- { type: 'startPollingTrace' },
+ { type: 'receiveJobLogSuccess', payload: jobLogPayload },
+ { type: 'startPollingJobLog' },
],
done,
);
});
- it('does not dispatch startPollingTrace when timeout is non-empty', (done) => {
- mockedState.traceTimeout = 1;
+ it('does not dispatch startPollingJobLog when timeout is non-empty', (done) => {
+ mockedState.jobLogTimeout = 1;
testAction(
- fetchTrace,
+ fetchJobLog,
null,
mockedState,
[],
[
{ type: 'toggleScrollisInBottom', payload: true },
- { type: 'receiveTraceSuccess', payload: tracePayload },
+ { type: 'receiveJobLogSuccess', payload: jobLogPayload },
],
done,
);
@@ -293,15 +293,15 @@ describe('Job State actions', () => {
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
});
- it('dispatches requestTrace and receiveTraceError ', (done) => {
+ it('dispatches requestJobLog and receiveJobLogError ', (done) => {
testAction(
- fetchTrace,
+ fetchJobLog,
null,
mockedState,
[],
[
{
- type: 'receiveTraceError',
+ type: 'receiveJobLogError',
},
],
done,
@@ -310,7 +310,7 @@ describe('Job State actions', () => {
});
});
- describe('startPollingTrace', () => {
+ describe('startPollingJobLog', () => {
let dispatch;
let commit;
@@ -318,18 +318,18 @@ describe('Job State actions', () => {
dispatch = jest.fn();
commit = jest.fn();
- startPollingTrace({ dispatch, commit });
+ startPollingJobLog({ dispatch, commit });
});
afterEach(() => {
jest.clearAllTimers();
});
- it('should save the timeout id but not call fetchTrace', () => {
- expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, expect.any(Number));
+ it('should save the timeout id but not call fetchJobLog', () => {
+ expect(commit).toHaveBeenCalledWith(types.SET_JOB_LOG_TIMEOUT, expect.any(Number));
expect(commit.mock.calls[0][1]).toBeGreaterThan(0);
- expect(dispatch).not.toHaveBeenCalledWith('fetchTrace');
+ expect(dispatch).not.toHaveBeenCalledWith('fetchJobLog');
});
describe('after timeout has passed', () => {
@@ -337,14 +337,14 @@ describe('Job State actions', () => {
jest.advanceTimersByTime(4000);
});
- it('should clear the timeout id and fetchTrace', () => {
- expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 0);
- expect(dispatch).toHaveBeenCalledWith('fetchTrace');
+ it('should clear the timeout id and fetchJobLog', () => {
+ expect(commit).toHaveBeenCalledWith(types.SET_JOB_LOG_TIMEOUT, 0);
+ expect(dispatch).toHaveBeenCalledWith('fetchJobLog');
});
});
});
- describe('stopPollingTrace', () => {
+ describe('stopPollingJobLog', () => {
let origTimeout;
beforeEach(() => {
@@ -358,40 +358,40 @@ describe('Job State actions', () => {
window.clearTimeout = origTimeout;
});
- it('should commit STOP_POLLING_TRACE mutation ', (done) => {
- const traceTimeout = 7;
+ it('should commit STOP_POLLING_JOB_LOG mutation ', (done) => {
+ const jobLogTimeout = 7;
testAction(
- stopPollingTrace,
+ stopPollingJobLog,
null,
- { ...mockedState, traceTimeout },
- [{ type: types.SET_TRACE_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_TRACE }],
+ { ...mockedState, jobLogTimeout },
+ [{ type: types.SET_JOB_LOG_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_JOB_LOG }],
[],
)
.then(() => {
- expect(window.clearTimeout).toHaveBeenCalledWith(traceTimeout);
+ expect(window.clearTimeout).toHaveBeenCalledWith(jobLogTimeout);
})
.then(done)
.catch(done.fail);
});
});
- describe('receiveTraceSuccess', () => {
- it('should commit RECEIVE_TRACE_SUCCESS mutation ', (done) => {
+ describe('receiveJobLogSuccess', () => {
+ it('should commit RECEIVE_JOB_LOG_SUCCESS mutation ', (done) => {
testAction(
- receiveTraceSuccess,
+ receiveJobLogSuccess,
'hello world',
mockedState,
- [{ type: types.RECEIVE_TRACE_SUCCESS, payload: 'hello world' }],
+ [{ type: types.RECEIVE_JOB_LOG_SUCCESS, payload: 'hello world' }],
[],
done,
);
});
});
- describe('receiveTraceError', () => {
- it('should commit stop polling trace', (done) => {
- testAction(receiveTraceError, null, mockedState, [], [{ type: 'stopPollingTrace' }], done);
+ describe('receiveJobLogError', () => {
+ it('should commit stop polling job log', (done) => {
+ testAction(receiveJobLogError, null, mockedState, [], [{ type: 'stopPollingJobLog' }], done);
});
});
diff --git a/spec/frontend/jobs/store/getters_spec.js b/spec/frontend/jobs/store/getters_spec.js
index 379114c3737..f26c0cf00fd 100644
--- a/spec/frontend/jobs/store/getters_spec.js
+++ b/spec/frontend/jobs/store/getters_spec.js
@@ -102,13 +102,13 @@ describe('Job Store Getters', () => {
});
});
- describe('hasTrace', () => {
+ describe('hasJobLog', () => {
describe('when has_trace is true', () => {
it('returns true', () => {
localState.job.has_trace = true;
localState.job.status = {};
- expect(getters.hasTrace(localState)).toEqual(true);
+ expect(getters.hasJobLog(localState)).toEqual(true);
});
});
@@ -117,7 +117,7 @@ describe('Job Store Getters', () => {
localState.job.has_trace = false;
localState.job.status = { group: 'running' };
- expect(getters.hasTrace(localState)).toEqual(true);
+ expect(getters.hasJobLog(localState)).toEqual(true);
});
});
@@ -126,7 +126,7 @@ describe('Job Store Getters', () => {
localState.job.has_trace = false;
localState.job.status = { group: 'pending' };
- expect(getters.hasTrace(localState)).toEqual(false);
+ expect(getters.hasJobLog(localState)).toEqual(false);
});
});
});
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index 159315330e4..b73aa8abf4e 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -45,39 +45,39 @@ describe('Jobs Store Mutations', () => {
});
});
- describe('RECEIVE_TRACE_SUCCESS', () => {
- describe('when trace has state', () => {
- it('sets traceState', () => {
+ describe('RECEIVE_JOB_LOG_SUCCESS', () => {
+ describe('when job log has state', () => {
+ it('sets jobLogState', () => {
const stateLog =
'eyJvZmZzZXQiOjczNDQ1MSwibl9vcGVuX3RhZ3MiOjAsImZnX2NvbG9yIjpudWxsLCJiZ19jb2xvciI6bnVsbCwic3R5bGVfbWFzayI6MH0=';
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
state: stateLog,
});
- expect(stateCopy.traceState).toEqual(stateLog);
+ expect(stateCopy.jobLogState).toEqual(stateLog);
});
});
- describe('when traceSize is smaller than the total size', () => {
- it('sets isTraceSizeVisible to true', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { total: 51184600, size: 1231 });
+ describe('when jobLogSize is smaller than the total size', () => {
+ it('sets isJobLogSizeVisible to true', () => {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { total: 51184600, size: 1231 });
- expect(stateCopy.isTraceSizeVisible).toEqual(true);
+ expect(stateCopy.isJobLogSizeVisible).toEqual(true);
});
});
- describe('when traceSize is bigger than the total size', () => {
- it('sets isTraceSizeVisible to false', () => {
- const copy = { ...stateCopy, traceSize: 5118460, size: 2321312 };
+ describe('when jobLogSize is bigger than the total size', () => {
+ it('sets isJobLogSizeVisible to false', () => {
+ const copy = { ...stateCopy, jobLogSize: 5118460, size: 2321312 };
- mutations[types.RECEIVE_TRACE_SUCCESS](copy, { total: 511846 });
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](copy, { total: 511846 });
- expect(copy.isTraceSizeVisible).toEqual(false);
+ expect(copy.isJobLogSizeVisible).toEqual(false);
});
});
- it('sets trace, trace size and isTraceComplete', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ it('sets job log size and isJobLogComplete', () => {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: true,
html,
size: 511846,
@@ -85,15 +85,15 @@ describe('Jobs Store Mutations', () => {
lines: [],
});
- expect(stateCopy.traceSize).toEqual(511846);
- expect(stateCopy.isTraceComplete).toEqual(true);
+ expect(stateCopy.jobLogSize).toEqual(511846);
+ expect(stateCopy.isJobLogComplete).toEqual(true);
});
describe('with new job log', () => {
describe('log.lines', () => {
describe('when append is true', () => {
it('sets the parsed log ', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: true,
size: 511846,
complete: true,
@@ -105,7 +105,7 @@ describe('Jobs Store Mutations', () => {
],
});
- expect(stateCopy.trace).toEqual([
+ expect(stateCopy.jobLog).toEqual([
{
offset: 1,
content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
@@ -117,7 +117,7 @@ describe('Jobs Store Mutations', () => {
describe('when it is defined', () => {
it('sets the parsed log ', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: false,
size: 511846,
complete: true,
@@ -126,7 +126,7 @@ describe('Jobs Store Mutations', () => {
],
});
- expect(stateCopy.trace).toEqual([
+ expect(stateCopy.jobLog).toEqual([
{
offset: 0,
content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
@@ -138,7 +138,7 @@ describe('Jobs Store Mutations', () => {
describe('when it is null', () => {
it('sets the default value', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: true,
html,
size: 511846,
@@ -146,30 +146,30 @@ describe('Jobs Store Mutations', () => {
lines: null,
});
- expect(stateCopy.trace).toEqual([]);
+ expect(stateCopy.jobLog).toEqual([]);
});
});
});
});
});
- describe('SET_TRACE_TIMEOUT', () => {
- it('sets the traceTimeout id', () => {
+ describe('SET_JOB_LOG_TIMEOUT', () => {
+ it('sets the jobLogTimeout id', () => {
const id = 7;
- expect(stateCopy.traceTimeout).not.toEqual(id);
+ expect(stateCopy.jobLogTimeout).not.toEqual(id);
- mutations[types.SET_TRACE_TIMEOUT](stateCopy, id);
+ mutations[types.SET_JOB_LOG_TIMEOUT](stateCopy, id);
- expect(stateCopy.traceTimeout).toEqual(id);
+ expect(stateCopy.jobLogTimeout).toEqual(id);
});
});
- describe('STOP_POLLING_TRACE', () => {
- it('sets isTraceComplete to true', () => {
- mutations[types.STOP_POLLING_TRACE](stateCopy);
+ describe('STOP_POLLING_JOB_LOG', () => {
+ it('sets isJobLogComplete to true', () => {
+ mutations[types.STOP_POLLING_JOB_LOG](stateCopy);
- expect(stateCopy.isTraceComplete).toEqual(true);
+ expect(stateCopy.isJobLogComplete).toEqual(true);
});
});
@@ -296,12 +296,12 @@ describe('Job Store mutations, feature flag ON', () => {
window.gon = origGon;
});
- describe('RECEIVE_TRACE_SUCCESS', () => {
+ describe('RECEIVE_JOB_LOG_SUCCESS', () => {
describe('with new job log', () => {
describe('log.lines', () => {
describe('when append is true', () => {
it('sets the parsed log ', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: true,
size: 511846,
complete: true,
@@ -313,7 +313,7 @@ describe('Job Store mutations, feature flag ON', () => {
],
});
- expect(stateCopy.trace).toEqual([
+ expect(stateCopy.jobLog).toEqual([
{
offset: 1,
content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
@@ -325,7 +325,7 @@ describe('Job Store mutations, feature flag ON', () => {
describe('when lines are defined', () => {
it('sets the parsed log ', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: false,
size: 511846,
complete: true,
@@ -334,7 +334,7 @@ describe('Job Store mutations, feature flag ON', () => {
],
});
- expect(stateCopy.trace).toEqual([
+ expect(stateCopy.jobLog).toEqual([
{
offset: 0,
content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
@@ -346,7 +346,7 @@ describe('Job Store mutations, feature flag ON', () => {
describe('when lines are null', () => {
it('sets the default value', () => {
- mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
+ mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
append: true,
html,
size: 511846,
@@ -354,7 +354,7 @@ describe('Job Store mutations, feature flag ON', () => {
lines: null,
});
- expect(stateCopy.trace).toEqual([]);
+ expect(stateCopy.jobLog).toEqual([]);
});
});
});
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 0c5fa150002..92ac33c8792 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -1,7 +1,7 @@
import {
logLinesParser,
logLinesParserLegacy,
- updateIncrementalTrace,
+ updateIncrementalJobLog,
parseHeaderLine,
parseLine,
addDurationToHeader,
@@ -487,11 +487,11 @@ describe('Jobs Store Utils', () => {
});
});
- describe('updateIncrementalTrace', () => {
+ describe('updateIncrementalJobLog', () => {
describe('without repeated section', () => {
it('concats and parses both arrays', () => {
const oldLog = logLinesParserLegacy(originalTrace);
- const result = updateIncrementalTrace(regularIncremental, oldLog);
+ const result = updateIncrementalJobLog(regularIncremental, oldLog);
expect(result).toEqual([
{
@@ -519,7 +519,7 @@ describe('Jobs Store Utils', () => {
describe('with regular line repeated offset', () => {
it('updates the last line and formats with the incremental part', () => {
const oldLog = logLinesParserLegacy(originalTrace);
- const result = updateIncrementalTrace(regularIncrementalRepeated, oldLog);
+ const result = updateIncrementalJobLog(regularIncrementalRepeated, oldLog);
expect(result).toEqual([
{
@@ -538,7 +538,7 @@ describe('Jobs Store Utils', () => {
describe('with header line repeated', () => {
it('updates the header line and formats with the incremental part', () => {
const oldLog = logLinesParserLegacy(headerTrace);
- const result = updateIncrementalTrace(headerTraceIncremental, oldLog);
+ const result = updateIncrementalJobLog(headerTraceIncremental, oldLog);
expect(result).toEqual([
{
@@ -564,7 +564,7 @@ describe('Jobs Store Utils', () => {
describe('with collapsible line repeated', () => {
it('updates the collapsible line and formats with the incremental part', () => {
const oldLog = logLinesParserLegacy(collapsibleTrace);
- const result = updateIncrementalTrace(collapsibleTraceIncremental, oldLog);
+ const result = updateIncrementalJobLog(collapsibleTraceIncremental, oldLog);
expect(result).toEqual([
{
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
new file mode 100644
index 00000000000..852106db44e
--- /dev/null
+++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
@@ -0,0 +1,155 @@
+import { ApolloLink, Observable } from 'apollo-link';
+import waitForPromises from 'helpers/wait_for_promises';
+import { getSuppressNetworkErrorsDuringNavigationLink } from '~/lib/apollo/suppress_network_errors_during_navigation_link';
+import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
+
+jest.mock('~/lib/utils/is_navigating_away');
+
+describe('getSuppressNetworkErrorsDuringNavigationLink', () => {
+ const originalGon = window.gon;
+ let subscription;
+
+ beforeEach(() => {
+ window.gon = originalGon;
+ });
+
+ afterEach(() => {
+ if (subscription) {
+ subscription.unsubscribe();
+ }
+ });
+
+ const makeMockGraphQLErrorLink = () =>
+ new ApolloLink(() =>
+ Observable.of({
+ errors: [
+ {
+ message: 'foo',
+ },
+ ],
+ }),
+ );
+
+ const makeMockNetworkErrorLink = () =>
+ new ApolloLink(
+ () =>
+ new Observable(() => {
+ throw new Error('NetworkError');
+ }),
+ );
+
+ const makeMockSuccessLink = () =>
+ new ApolloLink(() => Observable.of({ data: { foo: { id: 1 } } }));
+
+ const createSubscription = (otherLink, observer) => {
+ const mockOperation = { operationName: 'foo' };
+ const link = getSuppressNetworkErrorsDuringNavigationLink().concat(otherLink);
+ subscription = link.request(mockOperation).subscribe(observer);
+ };
+
+ describe('when disabled', () => {
+ it('returns null', () => {
+ expect(getSuppressNetworkErrorsDuringNavigationLink()).toBe(null);
+ });
+ });
+
+ 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();
+
+ 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);
+ });
+
+ 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 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);
+ });
+
+ 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(),
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap b/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap
index 791ec05befd..0b156049dab 100644
--- a/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap
+++ b/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`~/lib/logger/hello logHello console logs a friendly hello message 1`] = `
+exports[`~/lib/logger/hello logHello when on dot_com console logs a friendly hello message including the careers page 1`] = `
Array [
Array [
"%cWelcome to GitLab!%c
@@ -8,7 +8,24 @@ Array [
Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute!
🤝 Contribute to GitLab: https://about.gitlab.com/community/contribute/
-🔎 Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new",
+🔎 Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new
+🚀 We like your curiosity! Help us improve GitLab by joining the team: https://about.gitlab.com/jobs/",
+ "padding-top: 0.5em; font-size: 2em;",
+ "padding-bottom: 0.5em;",
+ ],
+]
+`;
+
+exports[`~/lib/logger/hello logHello when on self managed console logs a friendly hello message without including the careers page 1`] = `
+Array [
+ Array [
+ "%cWelcome to GitLab!%c
+
+Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute!
+
+🤝 Contribute to GitLab: https://about.gitlab.com/community/contribute/
+🔎 Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new
+",
"padding-top: 0.5em; font-size: 2em;",
"padding-bottom: 0.5em;",
],
diff --git a/spec/frontend/lib/logger/hello_spec.js b/spec/frontend/lib/logger/hello_spec.js
index 39abe0e0dd0..39c1b55313b 100644
--- a/spec/frontend/lib/logger/hello_spec.js
+++ b/spec/frontend/lib/logger/hello_spec.js
@@ -9,12 +9,32 @@ describe('~/lib/logger/hello', () => {
});
describe('logHello', () => {
- it('console logs a friendly hello message', () => {
- expect(consoleLogSpy).not.toHaveBeenCalled();
+ describe('when on dot_com', () => {
+ beforeEach(() => {
+ gon.dot_com = true;
+ });
- logHello();
+ it('console logs a friendly hello message including the careers page', () => {
+ expect(consoleLogSpy).not.toHaveBeenCalled();
- expect(consoleLogSpy.mock.calls).toMatchSnapshot();
+ logHello();
+
+ expect(consoleLogSpy.mock.calls).toMatchSnapshot();
+ });
+ });
+
+ describe('when on self managed', () => {
+ beforeEach(() => {
+ gon.dot_com = false;
+ });
+
+ it('console logs a friendly hello message without including the careers page', () => {
+ expect(consoleLogSpy).not.toHaveBeenCalled();
+
+ logHello();
+
+ expect(consoleLogSpy.mock.calls).toMatchSnapshot();
+ });
});
});
});
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index c6b88b2957c..87966cf9fba 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -1,4 +1,5 @@
import {
+ isValidColorExpression,
textColorForBackground,
hexToRgb,
validateHexColor,
@@ -72,4 +73,21 @@ describe('Color utils', () => {
},
);
});
+
+ describe('isValidColorExpression', () => {
+ it.each`
+ colorExpression | valid | desc
+ ${'#F00'} | ${true} | ${'valid'}
+ ${'rgba(0,0,0,0)'} | ${true} | ${'valid'}
+ ${'hsl(540,70%,50%)'} | ${true} | ${'valid'}
+ ${'red'} | ${true} | ${'valid'}
+ ${'F00'} | ${false} | ${'invalid'}
+ ${'F00'} | ${false} | ${'invalid'}
+ ${'gba(0,0,0,0)'} | ${false} | ${'invalid'}
+ ${'hls(540,70%,50%)'} | ${false} | ${'invalid'}
+ ${'hello'} | ${false} | ${'invalid'}
+ `('color expression $colorExpression is $desc', ({ colorExpression, valid }) => {
+ expect(isValidColorExpression(colorExpression)).toBe(valid);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index 942ba56196e..1adc70450e8 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -118,3 +118,18 @@ describe('date_format_utility.js', () => {
});
});
});
+
+describe('formatTimeAsSummary', () => {
+ it.each`
+ unit | value | result
+ ${'months'} | ${1.5} | ${'1.5M'}
+ ${'weeks'} | ${1.25} | ${'1.5w'}
+ ${'days'} | ${2} | ${'2d'}
+ ${'hours'} | ${10} | ${'10h'}
+ ${'minutes'} | ${20} | ${'20m'}
+ ${'seconds'} | ${10} | ${'<1m'}
+ ${'seconds'} | ${0} | ${'-'}
+ `('will format $value $unit to $result', ({ unit, value, result }) => {
+ expect(utils.formatTimeAsSummary({ [unit]: value })).toBe(result);
+ });
+});
diff --git a/spec/frontend/lib/utils/is_navigating_away_spec.js b/spec/frontend/lib/utils/is_navigating_away_spec.js
new file mode 100644
index 00000000000..e1230fe96bf
--- /dev/null
+++ b/spec/frontend/lib/utils/is_navigating_away_spec.js
@@ -0,0 +1,23 @@
+import { isNavigatingAway, setNavigatingForTestsOnly } from '~/lib/utils/is_navigating_away';
+
+describe('isNavigatingAway', () => {
+ beforeEach(() => {
+ // Make sure each test starts with the same state
+ setNavigatingForTestsOnly(false);
+ });
+
+ it.each([false, true])('it returns the navigation flag with value %s', (flag) => {
+ setNavigatingForTestsOnly(flag);
+ expect(isNavigatingAway()).toEqual(flag);
+ });
+
+ describe('when the browser starts navigating away', () => {
+ it('returns true', () => {
+ expect(isNavigatingAway()).toEqual(false);
+
+ window.dispatchEvent(new Event('beforeunload'));
+
+ expect(isNavigatingAway()).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 1f3659b5c76..9570d2a831c 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -363,4 +363,25 @@ describe('text_utility', () => {
expect(textUtils.insertFinalNewline(input, '\r\n')).toBe(output);
});
});
+
+ describe('escapeShellString', () => {
+ it.each`
+ character | input | output
+ ${'"'} | ${'";echo "you_shouldnt_run_this'} | ${'\'";echo "you_shouldnt_run_this\''}
+ ${'$'} | ${'$IFS'} | ${"'$IFS'"}
+ ${'\\'} | ${'evil-branch-name\\'} | ${"'evil-branch-name\\'"}
+ ${'!'} | ${'!event'} | ${"'!event'"}
+ `(
+ 'should not escape the $character character but wrap in single-quotes',
+ ({ input, output }) => {
+ expect(textUtils.escapeShellString(input)).toBe(output);
+ },
+ );
+
+ it("should escape the ' character and wrap in single-quotes", () => {
+ expect(textUtils.escapeShellString("fix-'bug-behavior'")).toBe(
+ "'fix-'\\''bug-behavior'\\'''",
+ );
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 6f186ba3227..18b68d91e01 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1004,4 +1004,39 @@ describe('URL utility', () => {
expect(urlUtils.isSameOriginUrl(url)).toBe(expected);
});
});
+
+ describe('constructWebIDEPath', () => {
+ let originalGl;
+ const projectIDEPath = '/foo/bar';
+ const sourceProj = 'my_-fancy-proj/boo';
+ const targetProj = 'boo/another-fancy-proj';
+ const mrIid = '7';
+
+ beforeEach(() => {
+ originalGl = window.gl;
+ window.gl = { webIDEPath: projectIDEPath };
+ });
+
+ afterEach(() => {
+ window.gl = originalGl;
+ });
+
+ it.each`
+ sourceProjectFullPath | targetProjectFullPath | iid | expectedPath
+ ${undefined} | ${undefined} | ${undefined} | ${projectIDEPath}
+ ${undefined} | ${undefined} | ${mrIid} | ${projectIDEPath}
+ ${undefined} | ${targetProj} | ${undefined} | ${projectIDEPath}
+ ${undefined} | ${targetProj} | ${mrIid} | ${projectIDEPath}
+ ${sourceProj} | ${undefined} | ${undefined} | ${projectIDEPath}
+ ${sourceProj} | ${targetProj} | ${undefined} | ${projectIDEPath}
+ ${sourceProj} | ${undefined} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`}
+ ${sourceProj} | ${sourceProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`}
+ ${sourceProj} | ${targetProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=${encodeURIComponent(targetProj)}`}
+ `(
+ 'returns $expectedPath for "$sourceProjectFullPath + $targetProjectFullPath + $iid"',
+ ({ expectedPath, ...args } = {}) => {
+ expect(urlUtils.constructWebIDEPath(args)).toBe(expectedPath);
+ },
+ );
+ });
});
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index d8453d453e7..7eb0ea37fe6 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -45,7 +45,7 @@ describe('RemoveMemberButton', () => {
title: 'Remove member',
isAccessRequest: true,
isInvite: true,
- oncallSchedules: { name: 'user', schedules: [] },
+ userDeletionObstacles: { name: 'user', obstacles: [] },
...propsData,
},
directives: {
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
index 0aa3780f030..10e451376c8 100644
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import { member, orphanedMember } from '../../mock_data';
describe('UserActionButtons', () => {
@@ -45,9 +46,9 @@ describe('UserActionButtons', () => {
isAccessRequest: false,
isInvite: false,
icon: 'remove',
- oncallSchedules: {
+ userDeletionObstacles: {
name: member.user.name,
- schedules: member.user.oncallSchedules,
+ obstacles: parseUserDeletionObstacles(member.user),
},
});
});
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index 1dc913e5c78..f755f08dbf2 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -6,7 +6,8 @@ import { nextTick } from 'vue';
import Vuex from 'vuex';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import { member } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -51,7 +52,7 @@ describe('LeaveModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => findModal().findComponent(GlForm);
- const findOncallSchedulesList = () => findModal().findComponent(OncallSchedulesList);
+ const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList);
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
@@ -89,25 +90,27 @@ describe('LeaveModal', () => {
);
});
- describe('On-call schedules list', () => {
- it("displays oncall schedules list when member's user is part of on-call schedules ", () => {
- const schedulesList = findOncallSchedulesList();
- expect(schedulesList.exists()).toBe(true);
- expect(schedulesList.props()).toMatchObject({
+ describe('User deletion obstacles list', () => {
+ it("displays obstacles list when member's user is part of on-call management", () => {
+ const obstaclesList = findUserDeletionObstaclesList();
+ expect(obstaclesList.exists()).toBe(true);
+ expect(obstaclesList.props()).toMatchObject({
isCurrentUser: true,
- schedules: member.user.oncallSchedules,
+ obstacles: parseUserDeletionObstacles(member.user),
});
});
- it("does NOT display oncall schedules list when member's user is NOT a part of on-call schedules ", async () => {
+ it("does NOT display obstacles list when member's user is NOT a part of on-call management", async () => {
wrapper.destroy();
- const memberWithoutOncallSchedules = cloneDeep(member);
- delete memberWithoutOncallSchedules.user.oncallSchedules;
- createComponent({ member: memberWithoutOncallSchedules });
+ const memberWithoutOncall = cloneDeep(member);
+ delete memberWithoutOncall.user.oncallSchedules;
+ delete memberWithoutOncall.user.escalationPolicies;
+
+ createComponent({ member: memberWithoutOncall });
await nextTick();
- expect(findOncallSchedulesList().exists()).toBe(false);
+ expect(findUserDeletionObstaclesList().exists()).toBe(false);
});
});
diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js
index 1dc41582c12..1d39c4b3175 100644
--- a/spec/frontend/members/components/modals/remove_member_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js
@@ -4,15 +4,19 @@ import Vue from 'vue';
import Vuex from 'vuex';
import RemoveMemberModal from '~/members/components/modals/remove_member_modal.vue';
import { MEMBER_TYPES } from '~/members/constants';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
Vue.use(Vuex);
describe('RemoveMemberModal', () => {
const memberPath = '/gitlab-org/gitlab-test/-/project_members/90';
- const mockSchedules = {
+ const mockObstacles = {
name: 'User1',
- schedules: [{ id: 1, name: 'Schedule 1' }],
+ obstacles: [
+ { name: 'Schedule 1', type: OBSTACLE_TYPES.oncallSchedules },
+ { name: 'Policy 1', type: OBSTACLE_TYPES.escalationPolicies },
+ ],
};
let wrapper;
@@ -44,18 +48,18 @@ describe('RemoveMemberModal', () => {
const findForm = () => wrapper.find({ ref: 'form' });
const findGlModal = () => wrapper.findComponent(GlModal);
- const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList);
+ const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
afterEach(() => {
wrapper.destroy();
});
describe.each`
- state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules
- ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}}
- ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
- ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}}
- ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
+ state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
+ ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
+ ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${true}
+ ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}} | ${false}
+ ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${false}
`(
'when $state',
({
@@ -66,7 +70,8 @@ describe('RemoveMemberModal', () => {
message,
removeSubMembershipsCheckboxExpected,
unassignIssuablesCheckboxExpected,
- onCallSchedules,
+ userDeletionObstacles,
+ isPartOfOncall,
}) => {
beforeEach(() => {
createComponent({
@@ -75,12 +80,10 @@ describe('RemoveMemberModal', () => {
message,
memberPath,
memberType,
- onCallSchedules,
+ userDeletionObstacles,
});
});
- const isPartOfOncallSchedules = Boolean(isAccessRequest && onCallSchedules.schedules?.length);
-
it(`has the title ${actionText}`, () => {
expect(findGlModal().attributes('title')).toBe(actionText);
});
@@ -109,8 +112,8 @@ describe('RemoveMemberModal', () => {
);
});
- it(`shows ${isPartOfOncallSchedules ? 'all' : 'no'} related on-call schedules`, () => {
- expect(findOnCallSchedulesList().exists()).toBe(isPartOfOncallSchedules);
+ it(`shows ${isPartOfOncall ? 'all' : 'no'} related on-call schedules or policies`, () => {
+ expect(findUserDeletionObstaclesList().exists()).toBe(isPartOfOncall);
});
it('submits the form when the modal is submitted', () => {
diff --git a/spec/frontend/members/components/table/expires_at_spec.js b/spec/frontend/members/components/table/expires_at_spec.js
deleted file mode 100644
index 2b8e6ab8f2a..00000000000
--- a/spec/frontend/members/components/table/expires_at_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { within } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
-import { useFakeDate } from 'helpers/fake_date';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ExpiresAt from '~/members/components/table/expires_at.vue';
-
-describe('ExpiresAt', () => {
- // March 15th, 2020
- useFakeDate(2020, 2, 15);
-
- let wrapper;
-
- const createComponent = (propsData) => {
- wrapper = mount(ExpiresAt, {
- propsData,
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).getByText(text, options));
-
- const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when no expiration date is set', () => {
- it('displays "No expiration set"', () => {
- createComponent({ date: null });
-
- expect(getByText('No expiration set').exists()).toBe(true);
- });
- });
-
- describe('when expiration date is in the past', () => {
- let expiredText;
-
- beforeEach(() => {
- createComponent({ date: '2019-03-15T00:00:00.000' });
-
- expiredText = getByText('Expired');
- });
-
- it('displays "Expired"', () => {
- expect(expiredText.exists()).toBe(true);
- expect(expiredText.classes()).toContain('gl-text-red-500');
- });
-
- it('displays tooltip with formatted date', () => {
- const tooltipDirective = getTooltipDirective(expiredText);
-
- expect(tooltipDirective).not.toBeUndefined();
- expect(expiredText.attributes('title')).toBe('Mar 15, 2019 12:00am UTC');
- });
- });
-
- describe('when expiration date is in the future', () => {
- it.each`
- date | expected | warningColor
- ${'2020-03-23T00:00:00.000'} | ${'in 8 days'} | ${false}
- ${'2020-03-20T00:00:00.000'} | ${'in 5 days'} | ${true}
- ${'2020-03-16T00:00:00.000'} | ${'in 1 day'} | ${true}
- ${'2020-03-15T05:00:00.000'} | ${'in about 5 hours'} | ${true}
- ${'2020-03-15T01:00:00.000'} | ${'in about 1 hour'} | ${true}
- ${'2020-03-15T00:30:00.000'} | ${'in 30 minutes'} | ${true}
- ${'2020-03-15T00:01:15.000'} | ${'in 1 minute'} | ${true}
- ${'2020-03-15T00:00:15.000'} | ${'in less than a minute'} | ${true}
- `('displays "$expected"', ({ date, expected, warningColor }) => {
- createComponent({ date });
-
- const expiredText = getByText(expected);
-
- expect(expiredText.exists()).toBe(true);
-
- if (warningColor) {
- expect(expiredText.classes()).toContain('gl-text-orange-500');
- } else {
- expect(expiredText.classes()).not.toContain('gl-text-orange-500');
- }
- });
- });
-});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 6885da53b26..580e5edd652 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -1,22 +1,24 @@
import { GlBadge, GlPagination, GlTable } from '@gitlab/ui';
-import {
- getByText as getByTextHelper,
- getByTestId as getByTestIdHelper,
- within,
-} from '@testing-library/dom';
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
+import { createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import CreatedAt from '~/members/components/table/created_at.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
-import ExpiresAt from '~/members/components/table/expires_at.vue';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
-import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES } from '~/members/constants';
+import {
+ MEMBER_TYPES,
+ MEMBER_STATE_CREATED,
+ MEMBER_STATE_AWAITING,
+ MEMBER_STATE_ACTIVE,
+ USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_PENDING_OWNER_APPROVAL,
+ TAB_QUERY_PARAM_VALUES,
+} from '~/members/constants';
import * as initUserPopovers from '~/user_popovers';
import {
member as memberMock,
@@ -53,7 +55,7 @@ describe('MembersTable', () => {
};
const createComponent = (state, provide = {}) => {
- wrapper = mount(MembersTable, {
+ wrapper = mountExtended(MembersTable, {
localVue,
propsData: {
tabQueryParamValue: TAB_QUERY_PARAM_VALUES.invite,
@@ -68,7 +70,6 @@ describe('MembersTable', () => {
stubs: [
'member-avatar',
'member-source',
- 'expires-at',
'created-at',
'member-action-buttons',
'role-dropdown',
@@ -81,17 +82,11 @@ describe('MembersTable', () => {
const url = 'https://localhost/foo-bar/-/project_members?tab=invited';
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
- const getByTestId = (id, options) =>
- createWrapper(getByTestIdHelper(wrapper.element, id, options));
-
const findTable = () => wrapper.find(GlTable);
const findTableCellByMemberId = (tableCellLabel, memberId) =>
- getByTestId(`members-table-row-${memberId}`).find(
- `[data-label="${tableCellLabel}"][role="cell"]`,
- );
+ wrapper
+ .findByTestId(`members-table-row-${memberId}`)
+ .find(`[data-label="${tableCellLabel}"][role="cell"]`);
const findPagination = () => extendedWrapper(wrapper.find(GlPagination));
@@ -103,7 +98,6 @@ describe('MembersTable', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('fields', () => {
@@ -119,7 +113,6 @@ describe('MembersTable', () => {
${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
- ${'expires'} | ${'Access expires'} | ${memberMock} | ${ExpiresAt}
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
@@ -128,7 +121,7 @@ describe('MembersTable', () => {
tableFields: [field],
});
- expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
+ expect(wrapper.findByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
if (expectedComponent) {
expect(
@@ -137,11 +130,50 @@ describe('MembersTable', () => {
}
});
+ describe('Invited column', () => {
+ describe.each`
+ state | userState | expectedBadgeLabel
+ ${MEMBER_STATE_CREATED} | ${null} | ${''}
+ ${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
+ ${MEMBER_STATE_AWAITING} | ${''} | ${''}
+ ${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
+ ${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
+ ${MEMBER_STATE_ACTIVE} | ${null} | ${''}
+ ${MEMBER_STATE_ACTIVE} | ${'something_else'} | ${''}
+ `('Invited Badge', ({ state, userState, expectedBadgeLabel }) => {
+ it(`${
+ expectedBadgeLabel ? 'shows' : 'hides'
+ } invited badge if user status: '${userState}' and member state: '${state}'`, () => {
+ createComponent({
+ members: [
+ {
+ ...invite,
+ state,
+ invite: {
+ ...invite.invite,
+ userState,
+ },
+ },
+ ],
+ tableFields: ['invited'],
+ });
+
+ const invitedTab = wrapper.findByTestId('invited-badge');
+
+ if (expectedBadgeLabel) {
+ expect(invitedTab.text()).toBe(expectedBadgeLabel);
+ } else {
+ expect(invitedTab.exists()).toBe(false);
+ }
+ });
+ });
+ });
+
describe('"Actions" field', () => {
it('renders "Actions" field for screen readers', () => {
createComponent({ members: [memberCanUpdate], tableFields: ['actions'] });
- const actionField = getByTestId('col-actions');
+ const actionField = wrapper.findByTestId('col-actions');
expect(actionField.exists()).toBe(true);
expect(actionField.classes('gl-sr-only')).toBe(true);
@@ -154,7 +186,7 @@ describe('MembersTable', () => {
it('does not render the "Actions" field', () => {
createComponent({ tableFields: ['actions'] }, { currentUserId: null });
- expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null);
+ expect(wrapper.findByTestId('col-actions').exists()).toBe(false);
});
});
@@ -177,7 +209,7 @@ describe('MembersTable', () => {
it('renders the "Actions" field', () => {
createComponent({ members, tableFields: ['actions'] });
- expect(getByTestId('col-actions').exists()).toBe(true);
+ expect(wrapper.findByTestId('col-actions').exists()).toBe(true);
expect(findTableCellByMemberId('Actions', members[0].id).classes()).toStrictEqual([
'col-actions',
@@ -199,7 +231,7 @@ describe('MembersTable', () => {
it('does not render the "Actions" field', () => {
createComponent({ members, tableFields: ['actions'] });
- expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null);
+ expect(wrapper.findByTestId('col-actions').exists()).toBe(false);
});
});
});
@@ -209,7 +241,7 @@ describe('MembersTable', () => {
it('displays a "No members found" message', () => {
createComponent();
- expect(getByText('No members found').exists()).toBe(true);
+ expect(wrapper.findByText('No members found').exists()).toBe(true);
});
});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index eb9f905fea2..f42ee295511 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -1,4 +1,4 @@
-import { MEMBER_TYPES } from '~/members/constants';
+import { MEMBER_TYPES, MEMBER_STATE_CREATED } from '~/members/constants';
export const member = {
requestedAt: null,
@@ -14,6 +14,7 @@ export const member = {
webUrl: 'https://gitlab.com/groups/foo-bar',
},
type: 'GroupMember',
+ state: MEMBER_STATE_CREATED,
user: {
id: 123,
name: 'Administrator',
@@ -23,6 +24,7 @@ export const member = {
blocked: false,
twoFactorEnabled: false,
oncallSchedules: [{ name: 'schedule 1' }],
+ escalationPolicies: [{ name: 'policy 1' }],
},
id: 238,
createdAt: '2020-07-17T16:22:46.923Z',
@@ -63,12 +65,13 @@ export const modalData = {
memberPath: '/groups/foo-bar/-/group_members/1',
memberType: 'GroupMember',
message: 'Are you sure you want to remove John Smith?',
- oncallSchedules: { name: 'user', schedules: [] },
+ userDeletionObstacles: { name: 'user', obstacles: [] },
};
const { user, ...memberNoUser } = member;
export const invite = {
...memberNoUser,
+ state: MEMBER_STATE_CREATED,
invite: {
email: 'jewel@hudsonwalter.biz',
avatarUrl: 'https://www.gravatar.com/avatar/cbab7510da7eec2f60f638261b05436d?s=80&d=identicon',
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 05538dbaeee..47b6c463377 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -37,6 +37,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
category="primary"
class="flex-grow-1"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
data-qa-selector="environments_dropdown"
headertext=""
hideheaderborder="true"
@@ -44,7 +45,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
highlighteditemstitleclass="gl-px-5"
id="monitor-environments-dropdown"
menu-class="monitor-environment-dropdown-menu"
- showhighlighteditemstitle="true"
size="medium"
text="production"
toggleclass="dropdown-menu-toggle"
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
index d20a111c701..6a19815883a 100644
--- a/spec/frontend/monitoring/fixture_data.js
+++ b/spec/frontend/monitoring/fixture_data.js
@@ -1,3 +1,4 @@
+import fixture from 'test_fixtures/metrics_dashboard/environment_metrics_dashboard.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { metricStates } from '~/monitoring/constants';
import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
@@ -5,10 +6,7 @@ import { stateAndPropsFromDataset } from '~/monitoring/utils';
import { metricsResult } from './mock_data';
-// Use globally available `getJSONFixture` so this file can be imported by both karma and jest specs
-export const metricsDashboardResponse = getJSONFixture(
- 'metrics_dashboard/environment_metrics_dashboard.json',
-);
+export const metricsDashboardResponse = fixture;
export const metricsDashboardPayload = metricsDashboardResponse.dashboard;
diff --git a/spec/frontend/namespace_select_spec.js b/spec/frontend/namespace_select_spec.js
deleted file mode 100644
index a38508dd601..00000000000
--- a/spec/frontend/namespace_select_spec.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import NamespaceSelect from '~/namespace_select';
-
-jest.mock('~/deprecated_jquery_dropdown');
-
-describe('NamespaceSelect', () => {
- it('initializes deprecatedJQueryDropdown', () => {
- const dropdown = document.createElement('div');
-
- // eslint-disable-next-line no-new
- new NamespaceSelect({ dropdown });
-
- expect(initDeprecatedJQueryDropdown).toHaveBeenCalled();
- });
-
- describe('as input', () => {
- let deprecatedJQueryDropdownOptions;
-
- beforeEach(() => {
- const dropdown = document.createElement('div');
- // eslint-disable-next-line no-new
- new NamespaceSelect({ dropdown });
- [[, deprecatedJQueryDropdownOptions]] = initDeprecatedJQueryDropdown.mock.calls;
- });
-
- it('prevents click events', () => {
- const dummyEvent = new Event('dummy');
- jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {});
-
- // expect(foo).toContain('test');
- deprecatedJQueryDropdownOptions.clicked({ e: dummyEvent });
-
- expect(dummyEvent.preventDefault).toHaveBeenCalled();
- });
- });
-
- describe('as filter', () => {
- let deprecatedJQueryDropdownOptions;
-
- beforeEach(() => {
- const dropdown = document.createElement('div');
- dropdown.dataset.isFilter = 'true';
- // eslint-disable-next-line no-new
- new NamespaceSelect({ dropdown });
- [[, deprecatedJQueryDropdownOptions]] = initDeprecatedJQueryDropdown.mock.calls;
- });
-
- it('does not prevent click events', () => {
- const dummyEvent = new Event('dummy');
- jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {});
-
- deprecatedJQueryDropdownOptions.clicked({ e: dummyEvent });
-
- expect(dummyEvent.preventDefault).not.toHaveBeenCalled();
- });
-
- it('sets URL of dropdown items', () => {
- const dummyNamespace = { id: 'eal' };
-
- const itemUrl = deprecatedJQueryDropdownOptions.url(dummyNamespace);
-
- expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`);
- });
- });
-});
diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js
index e14767f2594..669bdc2f89a 100644
--- a/spec/frontend/notebook/cells/code_spec.js
+++ b/spec/frontend/notebook/cells/code_spec.js
@@ -1,14 +1,17 @@
import Vue from 'vue';
+import fixture from 'test_fixtures/blob/notebook/basic.json';
import CodeComponent from '~/notebook/cells/code.vue';
const Component = Vue.extend(CodeComponent);
describe('Code component', () => {
let vm;
+
let json;
beforeEach(() => {
- json = getJSONFixture('blob/notebook/basic.json');
+ // Clone fixture as it could be modified by tests
+ json = JSON.parse(JSON.stringify(fixture));
});
const setupComponent = (cell) => {
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index 707efa21528..36b1e91f15f 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -1,6 +1,9 @@
import { mount } from '@vue/test-utils';
import katex from 'katex';
import Vue from 'vue';
+import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json';
+import basicJson from 'test_fixtures/blob/notebook/basic.json';
+import mathJson from 'test_fixtures/blob/notebook/math.json';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
const Component = Vue.extend(MarkdownComponent);
@@ -35,7 +38,7 @@ describe('Markdown component', () => {
let json;
beforeEach(() => {
- json = getJSONFixture('blob/notebook/basic.json');
+ json = basicJson;
// eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
@@ -104,7 +107,7 @@ describe('Markdown component', () => {
describe('tables', () => {
beforeEach(() => {
- json = getJSONFixture('blob/notebook/markdown-table.json');
+ json = markdownTableJson;
});
it('renders images and text', () => {
@@ -135,7 +138,7 @@ describe('Markdown component', () => {
describe('katex', () => {
beforeEach(() => {
- json = getJSONFixture('blob/notebook/math.json');
+ json = mathJson;
});
it('renders multi-line katex', async () => {
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 2985abf0f4f..7ece73d375c 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
+import json from 'test_fixtures/blob/notebook/basic.json';
import CodeComponent from '~/notebook/cells/output/index.vue';
const Component = Vue.extend(CodeComponent);
describe('Output component', () => {
let vm;
- let json;
const createComponent = (output) => {
vm = new Component({
@@ -17,11 +17,6 @@ describe('Output component', () => {
vm.$mount();
};
- beforeEach(() => {
- // This is the output after rendering a jupyter notebook
- json = getJSONFixture('blob/notebook/basic.json');
- });
-
describe('text output', () => {
beforeEach((done) => {
const textType = json.cells[2];
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index 4d0dacaf37e..cd531d628b3 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -1,18 +1,13 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
+import json from 'test_fixtures/blob/notebook/basic.json';
+import jsonWithWorksheet from 'test_fixtures/blob/notebook/worksheets.json';
import Notebook from '~/notebook/index.vue';
const Component = Vue.extend(Notebook);
describe('Notebook component', () => {
let vm;
- let json;
- let jsonWithWorksheet;
-
- beforeEach(() => {
- json = getJSONFixture('blob/notebook/basic.json');
- jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json');
- });
function buildComponent(notebook) {
return mount(Component, {
diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js
index 5e1cb813369..8ac6144e5c8 100644
--- a/spec/frontend/notes/components/comment_type_dropdown_spec.js
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -47,8 +47,18 @@ describe('CommentTypeDropdown component', () => {
it('Should emit `change` event when clicking on an alternate dropdown option', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- findCommentDropdownOption().vm.$emit('click');
- findDiscussionDropdownOption().vm.$emit('click');
+ const event = {
+ type: 'click',
+ stopPropagation: jest.fn(),
+ preventDefault: jest.fn(),
+ };
+
+ findCommentDropdownOption().vm.$emit('click', event);
+ findDiscussionDropdownOption().vm.$emit('click', event);
+
+ // ensure the native events don't trigger anything
+ expect(event.stopPropagation).toHaveBeenCalledTimes(2);
+ expect(event.preventDefault).toHaveBeenCalledTimes(2);
expect(wrapper.emitted('change')[0]).toEqual([constants.COMMENT]);
expect(wrapper.emitted('change').length).toEqual(1);
diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js
index e997fc4da50..c352265654b 100644
--- a/spec/frontend/notes/components/diff_with_note_spec.js
+++ b/spec/frontend/notes/components/diff_with_note_spec.js
@@ -1,10 +1,9 @@
import { shallowMount } from '@vue/test-utils';
+import discussionFixture from 'test_fixtures/merge_requests/diff_discussion.json';
+import imageDiscussionFixture from 'test_fixtures/merge_requests/image_diff_discussion.json';
import { createStore } from '~/mr_notes/stores';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
-const discussionFixture = 'merge_requests/diff_discussion.json';
-const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
-
describe('diff_with_note', () => {
let store;
let wrapper;
@@ -35,7 +34,7 @@ describe('diff_with_note', () => {
describe('text diff', () => {
beforeEach(() => {
- const diffDiscussion = getJSONFixture(discussionFixture)[0];
+ const diffDiscussion = discussionFixture[0];
wrapper = shallowMount(DiffWithNote, {
propsData: {
@@ -75,7 +74,7 @@ describe('diff_with_note', () => {
describe('image diff', () => {
beforeEach(() => {
- const imageDiscussion = getJSONFixture(imageDiscussionFixture)[0];
+ const imageDiscussion = imageDiscussionFixture[0];
wrapper = shallowMount(DiffWithNote, {
propsData: { discussion: imageDiscussion, diffFile: {} },
store,
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 92137d3190f..abc888cd245 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -150,6 +150,16 @@ describe('issue_note_form component', () => {
expect(handleFormUpdate.length).toBe(1);
});
+
+ it('should disable textarea when ctrl+enter is pressed', async () => {
+ textarea.trigger('keydown.enter', { ctrlKey: true });
+
+ expect(textarea.attributes('disabled')).toBeUndefined();
+
+ await nextTick();
+
+ expect(textarea.attributes('disabled')).toBe('disabled');
+ });
});
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index a364a524e7b..727ef02dcbb 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
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 DiscussionNotes from '~/notes/components/discussion_notes.vue';
@@ -17,8 +18,6 @@ import {
userDataMock,
} from '../mock_data';
-const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
-
describe('noteable_discussion component', () => {
let store;
let wrapper;
@@ -119,7 +118,7 @@ describe('noteable_discussion component', () => {
describe('for resolved thread', () => {
beforeEach(() => {
- const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
+ const discussion = discussionWithTwoUnresolvedNotes[0];
wrapper.setProps({ discussion });
});
@@ -133,7 +132,7 @@ describe('noteable_discussion component', () => {
describe('for unresolved thread', () => {
beforeEach(() => {
const discussion = {
- ...getJSONFixture(discussionWithTwoUnresolvedNotes)[0],
+ ...discussionWithTwoUnresolvedNotes[0],
expanded: true,
};
discussion.resolved = false;
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 467a8bec21b..038aff3be04 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -189,6 +189,27 @@ describe('issue_note', () => {
createWrapper();
});
+ describe('avatar sizes in diffs', () => {
+ const line = {
+ line_code: 'abc_1_1',
+ type: null,
+ old_line: '1',
+ new_line: '1',
+ };
+
+ it('should render 24px avatars', async () => {
+ wrapper.setProps({
+ note: { ...note },
+ discussionRoot: true,
+ line,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.findComponent(UserAvatarLink).props('imgSize')).toBe(24);
+ });
+ });
+
it('should render user information', () => {
const { author } = note;
const avatar = wrapper.findComponent(UserAvatarLink);
diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js
index 3adb5da020e..9a11fdba508 100644
--- a/spec/frontend/notes/stores/getters_spec.js
+++ b/spec/frontend/notes/stores/getters_spec.js
@@ -1,3 +1,4 @@
+import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
import { DESC, ASC } from '~/notes/constants';
import * as getters from '~/notes/stores/getters';
import {
@@ -17,8 +18,6 @@ import {
draftDiffDiscussion,
} from '../mock_data';
-const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
-
// Helper function to ensure that we're using the same schema across tests.
const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({
discussionId,
@@ -123,7 +122,7 @@ describe('Getters Notes Store', () => {
describe('resolvedDiscussionsById', () => {
it('ignores unresolved system notes', () => {
- const [discussion] = getJSONFixture(discussionWithTwoUnresolvedNotes);
+ const [discussion] = discussionWithTwoUnresolvedNotes;
discussion.notes[0].resolved = true;
discussion.notes[1].resolved = false;
state.discussions.push(discussion);
diff --git a/spec/frontend/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 70bda1d9f9e..3187cbf6547 100644
--- a/spec/frontend/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
@@ -3,7 +3,7 @@ import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me';
describe('OAuthRememberMe', () => {
const findFormAction = (selector) => {
- return $(`#oauth-container .oauth-login${selector}`).parent('form').attr('action');
+ return $(`#oauth-container .js-oauth-login${selector}`).parent('form').attr('action');
};
beforeEach(() => {
diff --git a/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
deleted file mode 100644
index a3423e3f4d7..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
+++ /dev/null
@@ -1,36 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ConanInstallation renders all the messages 1`] = `
-<div>
- <installation-title-stub
- options="[object Object]"
- packagetype="conan"
- />
-
- <code-instruction-stub
- copytext="Copy Conan Command"
- instruction="foo/command"
- label="Conan Command"
- trackingaction="copy_conan_command"
- trackinglabel="code_instruction"
- />
-
- <h3
- class="gl-font-lg"
- >
- Registry setup
- </h3>
-
- <code-instruction-stub
- copytext="Copy Conan Setup Command"
- instruction="foo/setup"
- label="Add Conan Remote"
- trackingaction="copy_conan_setup_command"
- trackinglabel="code_instruction"
- />
-
- <gl-sprintf-stub
- message="For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
- />
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
deleted file mode 100644
index 39469bf4fd0..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
+++ /dev/null
@@ -1,34 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`DependencyRow renders full dependency 1`] = `
-<div
- class="gl-responsive-table-row"
->
- <div
- class="table-section section-50"
- >
- <strong
- class="gl-text-body"
- >
- Test.Dependency
- </strong>
-
- <span
- data-testid="target-framework"
- >
- (.NETStandard2.0)
- </span>
- </div>
-
- <div
- class="table-section section-50 gl-display-flex gl-md-justify-content-end"
- data-testid="version-pattern"
- >
- <span
- class="gl-text-body"
- >
- 2.3.7
- </span>
- </div>
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
deleted file mode 100644
index 8a2793c0010..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
+++ /dev/null
@@ -1,112 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MavenInstallation groovy renders all the messages 1`] = `
-<div>
- <installation-title-stub
- options="[object Object],[object Object],[object Object]"
- packagetype="maven"
- />
-
- <code-instruction-stub
- class="gl-mb-5"
- copytext="Copy Gradle Groovy DSL install command"
- instruction="foo/gradle/groovy/install"
- label="Gradle Groovy DSL install command"
- trackingaction="copy_gradle_install_command"
- trackinglabel="code_instruction"
- />
-
- <code-instruction-stub
- copytext="Copy add Gradle Groovy DSL repository command"
- instruction="foo/gradle/groovy/add/source"
- label="Add Gradle Groovy DSL repository command"
- multiline="true"
- trackingaction="copy_gradle_add_to_source_command"
- trackinglabel="code_instruction"
- />
-</div>
-`;
-
-exports[`MavenInstallation kotlin renders all the messages 1`] = `
-<div>
- <installation-title-stub
- options="[object Object],[object Object],[object Object]"
- packagetype="maven"
- />
-
- <code-instruction-stub
- class="gl-mb-5"
- copytext="Copy Gradle Kotlin DSL install command"
- instruction="foo/gradle/kotlin/install"
- label="Gradle Kotlin DSL install command"
- trackingaction="copy_kotlin_install_command"
- trackinglabel="code_instruction"
- />
-
- <code-instruction-stub
- copytext="Copy add Gradle Kotlin DSL repository command"
- instruction="foo/gradle/kotlin/add/source"
- label="Add Gradle Kotlin DSL repository command"
- multiline="true"
- trackingaction="copy_kotlin_add_to_source_command"
- trackinglabel="code_instruction"
- />
-</div>
-`;
-
-exports[`MavenInstallation maven renders all the messages 1`] = `
-<div>
- <installation-title-stub
- options="[object Object],[object Object],[object Object]"
- packagetype="maven"
- />
-
- <p>
- <gl-sprintf-stub
- message="Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
- />
- </p>
-
- <code-instruction-stub
- copytext="Copy Maven XML"
- instruction="foo/xml"
- label=""
- multiline="true"
- trackingaction="copy_maven_xml"
- trackinglabel="code_instruction"
- />
-
- <code-instruction-stub
- copytext="Copy Maven command"
- instruction="foo/command"
- label="Maven Command"
- trackingaction="copy_maven_command"
- trackinglabel="code_instruction"
- />
-
- <h3
- class="gl-font-lg"
- >
- Registry setup
- </h3>
-
- <p>
- <gl-sprintf-stub
- message="If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
- />
- </p>
-
- <code-instruction-stub
- copytext="Copy Maven registry XML"
- instruction="foo/setup"
- label=""
- multiline="true"
- trackingaction="copy_maven_setup_xml"
- trackinglabel="code_instruction"
- />
-
- <gl-sprintf-stub
- message="For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}."
- />
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
deleted file mode 100644
index 015c7b94dde..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
+++ /dev/null
@@ -1,36 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`NpmInstallation renders all the messages 1`] = `
-<div>
- <installation-title-stub
- options="[object Object],[object Object]"
- packagetype="npm"
- />
-
- <code-instruction-stub
- copytext="Copy npm command"
- instruction="npm i @Test/package"
- label=""
- trackingaction="copy_npm_install_command"
- trackinglabel="code_instruction"
- />
-
- <h3
- class="gl-font-lg"
- >
- Registry setup
- </h3>
-
- <code-instruction-stub
- copytext="Copy npm setup command"
- instruction="echo @Test:registry=undefined/ >> .npmrc"
- label=""
- trackingaction="copy_npm_setup_command"
- trackinglabel="code_instruction"
- />
-
- <gl-sprintf-stub
- message="You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
- />
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
deleted file mode 100644
index 04532743952..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
+++ /dev/null
@@ -1,36 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`NugetInstallation renders all the messages 1`] = `
-<div>
- <installation-title-stub
- options="[object Object]"
- packagetype="nuget"
- />
-
- <code-instruction-stub
- copytext="Copy NuGet Command"
- instruction="foo/command"
- label="NuGet Command"
- trackingaction="copy_nuget_install_command"
- trackinglabel="code_instruction"
- />
-
- <h3
- class="gl-font-lg"
- >
- Registry setup
- </h3>
-
- <code-instruction-stub
- copytext="Copy NuGet Setup Command"
- instruction="foo/setup"
- label="Add NuGet Source"
- trackingaction="copy_nuget_setup_command"
- trackinglabel="code_instruction"
- />
-
- <gl-sprintf-stub
- message="For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
- />
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
deleted file mode 100644
index 318cea98b92..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
+++ /dev/null
@@ -1,168 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PackageTitle renders with tags 1`] = `
-<div
- class="gl-display-flex gl-flex-direction-column"
- data-qa-selector="package_title"
->
- <div
- class="gl-display-flex gl-justify-content-space-between gl-py-3"
- >
- <div
- class="gl-flex-direction-column gl-flex-grow-1"
- >
- <div
- class="gl-display-flex"
- >
- <!---->
-
- <div
- class="gl-display-flex gl-flex-direction-column"
- >
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
- data-testid="title"
- >
- Test package
- </h1>
-
- <div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
- >
- <gl-icon-stub
- class="gl-mr-3"
- name="eye"
- size="16"
- />
-
- <gl-sprintf-stub
- message="v%{version} published %{timeAgo}"
- />
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-type"
- icon="package"
- link=""
- size="s"
- text="maven"
- texttooltip=""
- />
- </div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-size"
- icon="disk"
- link=""
- size="s"
- text="300 bytes"
- texttooltip=""
- />
- </div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <package-tags-stub
- hidelabel="true"
- tagdisplaylimit="2"
- tags="[object Object],[object Object],[object Object],[object Object]"
- />
- </div>
- </div>
- </div>
-
- <!---->
- </div>
-
- <p />
-</div>
-`;
-
-exports[`PackageTitle renders without tags 1`] = `
-<div
- class="gl-display-flex gl-flex-direction-column"
- data-qa-selector="package_title"
->
- <div
- class="gl-display-flex gl-justify-content-space-between gl-py-3"
- >
- <div
- class="gl-flex-direction-column gl-flex-grow-1"
- >
- <div
- class="gl-display-flex"
- >
- <!---->
-
- <div
- class="gl-display-flex gl-flex-direction-column"
- >
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
- data-testid="title"
- >
- Test package
- </h1>
-
- <div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
- >
- <gl-icon-stub
- class="gl-mr-3"
- name="eye"
- size="16"
- />
-
- <gl-sprintf-stub
- message="v%{version} published %{timeAgo}"
- />
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-type"
- icon="package"
- link=""
- size="s"
- text="maven"
- texttooltip=""
- />
- </div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-size"
- icon="disk"
- link=""
- size="s"
- text="300 bytes"
- texttooltip=""
- />
- </div>
- </div>
- </div>
-
- <!---->
- </div>
-
- <p />
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
deleted file mode 100644
index d5bb825d8d1..00000000000
--- a/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
+++ /dev/null
@@ -1,45 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PypiInstallation renders all the messages 1`] = `
-<div>
- <installation-title-stub
- options="[object Object]"
- packagetype="pypi"
- />
-
- <code-instruction-stub
- copytext="Copy Pip command"
- data-testid="pip-command"
- instruction="pip install"
- label="Pip Command"
- trackingaction="copy_pip_install_command"
- trackinglabel="code_instruction"
- />
-
- <h3
- class="gl-font-lg"
- >
- Registry setup
- </h3>
-
- <p>
- <gl-sprintf-stub
- message="If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
- />
- </p>
-
- <code-instruction-stub
- copytext="Copy .pypirc content"
- data-testid="pypi-setup-content"
- instruction="python setup"
- label=""
- multiline="true"
- trackingaction="copy_pypi_setup_command"
- trackinglabel="code_instruction"
- />
-
- <gl-sprintf-stub
- message="For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
- />
-</div>
-`;
diff --git a/spec/frontend/packages/details/components/additional_metadata_spec.js b/spec/frontend/packages/details/components/additional_metadata_spec.js
deleted file mode 100644
index b339aa84348..00000000000
--- a/spec/frontend/packages/details/components/additional_metadata_spec.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import component from '~/packages/details/components/additional_metadata.vue';
-import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
-
-import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';
-
-describe('Package Additional Metadata', () => {
- let wrapper;
- const defaultProps = {
- packageEntity: { ...mavenPackage },
- };
-
- const mountComponent = (props) => {
- wrapper = shallowMount(component, {
- propsData: { ...defaultProps, ...props },
- stubs: {
- DetailsRow,
- GlSprintf,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findTitle = () => wrapper.find('[data-testid="title"]');
- const findMainArea = () => wrapper.find('[data-testid="main"]');
- const findNugetSource = () => wrapper.find('[data-testid="nuget-source"]');
- const findNugetLicense = () => wrapper.find('[data-testid="nuget-license"]');
- const findConanRecipe = () => wrapper.find('[data-testid="conan-recipe"]');
- const findMavenApp = () => wrapper.find('[data-testid="maven-app"]');
- const findMavenGroup = () => wrapper.find('[data-testid="maven-group"]');
- const findElementLink = (container) => container.find(GlLink);
-
- it('has the correct title', () => {
- mountComponent();
-
- const title = findTitle();
-
- expect(title.exists()).toBe(true);
- expect(title.text()).toBe('Additional Metadata');
- });
-
- describe.each`
- packageEntity | visible | metadata
- ${mavenPackage} | ${true} | ${'maven_metadatum'}
- ${conanPackage} | ${true} | ${'conan_metadatum'}
- ${nugetPackage} | ${true} | ${'nuget_metadatum'}
- ${npmPackage} | ${false} | ${null}
- `('Component visibility', ({ packageEntity, visible, metadata }) => {
- it(`Is ${visible} that the component markup is visible when the package is ${packageEntity.package_type}`, () => {
- mountComponent({ packageEntity });
-
- expect(findTitle().exists()).toBe(visible);
- expect(findMainArea().exists()).toBe(visible);
- });
-
- it(`The component is hidden if ${metadata} is missing`, () => {
- mountComponent({ packageEntity: { ...packageEntity, [metadata]: null } });
-
- expect(findTitle().exists()).toBe(false);
- expect(findMainArea().exists()).toBe(false);
- });
- });
-
- describe('nuget metadata', () => {
- beforeEach(() => {
- mountComponent({ packageEntity: nugetPackage });
- });
-
- it.each`
- name | finderFunction | text | link | icon
- ${'source'} | ${findNugetSource} | ${'Source project located at project-foo-url'} | ${'project_url'} | ${'project'}
- ${'license'} | ${findNugetLicense} | ${'License information located at license-foo-url'} | ${'license_url'} | ${'license'}
- `('$name element', ({ finderFunction, text, link, icon }) => {
- const element = finderFunction();
- expect(element.exists()).toBe(true);
- expect(element.text()).toBe(text);
- expect(element.props('icon')).toBe(icon);
- expect(findElementLink(element).attributes('href')).toBe(nugetPackage.nuget_metadatum[link]);
- });
- });
-
- describe('conan metadata', () => {
- beforeEach(() => {
- mountComponent({ packageEntity: conanPackage });
- });
-
- it.each`
- name | finderFunction | text | icon
- ${'recipe'} | ${findConanRecipe} | ${'Recipe: conan-package/1.0.0@conan+conan-package/stable'} | ${'information-o'}
- `('$name element', ({ finderFunction, text, icon }) => {
- const element = finderFunction();
- expect(element.exists()).toBe(true);
- expect(element.text()).toBe(text);
- expect(element.props('icon')).toBe(icon);
- });
- });
-
- describe('maven metadata', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it.each`
- name | finderFunction | text | icon
- ${'app'} | ${findMavenApp} | ${'App name: test-app'} | ${'information-o'}
- ${'group'} | ${findMavenGroup} | ${'App group: com.test.app'} | ${'information-o'}
- `('$name element', ({ finderFunction, text, icon }) => {
- const element = finderFunction();
- expect(element.exists()).toBe(true);
- expect(element.text()).toBe(text);
- expect(element.props('icon')).toBe(icon);
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/composer_installation_spec.js b/spec/frontend/packages/details/components/composer_installation_spec.js
deleted file mode 100644
index 18d11c7dd57..00000000000
--- a/spec/frontend/packages/details/components/composer_installation_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { GlSprintf, GlLink } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { registryUrl as composerHelpPath } from 'jest/packages/details/mock_data';
-import { composerPackage as packageEntity } from 'jest/packages/mock_data';
-import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
-import InstallationTitle from '~/packages/details/components/installation_title.vue';
-
-import { TrackingActions } from '~/packages/details/constants';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('ComposerInstallation', () => {
- let wrapper;
- let store;
-
- const composerRegistryIncludeStr = 'foo/registry';
- const composerPackageIncludeStr = 'foo/package';
-
- const createStore = (groupExists = true) => {
- store = new Vuex.Store({
- state: { packageEntity, composerHelpPath },
- getters: {
- composerRegistryInclude: () => composerRegistryIncludeStr,
- composerPackageInclude: () => composerPackageIncludeStr,
- groupExists: () => groupExists,
- },
- });
- };
-
- const findRootNode = () => wrapper.find('[data-testid="root-node"]');
- const findRegistryInclude = () => wrapper.find('[data-testid="registry-include"]');
- const findPackageInclude = () => wrapper.find('[data-testid="package-include"]');
- const findHelpText = () => wrapper.find('[data-testid="help-text"]');
- const findHelpLink = () => wrapper.find(GlLink);
- const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
-
- function createComponent() {
- wrapper = shallowMount(ComposerInstallation, {
- localVue,
- store,
- stubs: {
- GlSprintf,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('install command switch', () => {
- it('has the installation title component', () => {
- createStore();
- createComponent();
-
- expect(findInstallationTitle().exists()).toBe(true);
- expect(findInstallationTitle().props()).toMatchObject({
- packageType: 'composer',
- options: [{ value: 'composer', label: 'Show Composer commands' }],
- });
- });
- });
-
- describe('registry include command', () => {
- beforeEach(() => {
- createStore();
- createComponent();
- });
-
- it('uses code_instructions', () => {
- const registryIncludeCommand = findRegistryInclude();
- expect(registryIncludeCommand.exists()).toBe(true);
- expect(registryIncludeCommand.props()).toMatchObject({
- instruction: composerRegistryIncludeStr,
- copyText: 'Copy registry include',
- trackingAction: TrackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
- });
- });
-
- it('has the correct title', () => {
- expect(findRegistryInclude().props('label')).toBe('Add composer registry');
- });
- });
-
- describe('package include command', () => {
- beforeEach(() => {
- createStore();
- createComponent();
- });
-
- it('uses code_instructions', () => {
- const registryIncludeCommand = findPackageInclude();
- expect(registryIncludeCommand.exists()).toBe(true);
- expect(registryIncludeCommand.props()).toMatchObject({
- instruction: composerPackageIncludeStr,
- copyText: 'Copy require package include',
- trackingAction: TrackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
- });
- });
-
- it('has the correct title', () => {
- expect(findPackageInclude().props('label')).toBe('Install package version');
- });
-
- it('has the correct help text', () => {
- expect(findHelpText().text()).toBe(
- 'For more information on Composer packages in GitLab, see the documentation.',
- );
- expect(findHelpLink().attributes()).toMatchObject({
- href: composerHelpPath,
- target: '_blank',
- });
- });
- });
-
- describe('root node', () => {
- it('is normally rendered', () => {
- createStore();
- createComponent();
-
- expect(findRootNode().exists()).toBe(true);
- });
-
- it('is not rendered when the group does not exist', () => {
- createStore(false);
- createComponent();
-
- expect(findRootNode().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/conan_installation_spec.js b/spec/frontend/packages/details/components/conan_installation_spec.js
deleted file mode 100644
index 78a7d265a21..00000000000
--- a/spec/frontend/packages/details/components/conan_installation_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import ConanInstallation from '~/packages/details/components/conan_installation.vue';
-import InstallationTitle from '~/packages/details/components/installation_title.vue';
-import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
-import { conanPackage as packageEntity } from '../../mock_data';
-import { registryUrl as conanPath } from '../mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('ConanInstallation', () => {
- let wrapper;
-
- const conanInstallationCommandStr = 'foo/command';
- const conanSetupCommandStr = 'foo/setup';
-
- const store = new Vuex.Store({
- state: {
- packageEntity,
- conanPath,
- },
- getters: {
- conanInstallationCommand: () => conanInstallationCommandStr,
- conanSetupCommand: () => conanSetupCommandStr,
- },
- });
-
- const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
- const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
-
- function createComponent() {
- wrapper = shallowMount(ConanInstallation, {
- localVue,
- store,
- });
- }
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders all the messages', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('install command switch', () => {
- it('has the installation title component', () => {
- expect(findInstallationTitle().exists()).toBe(true);
- expect(findInstallationTitle().props()).toMatchObject({
- packageType: 'conan',
- options: [{ value: 'conan', label: 'Show Conan commands' }],
- });
- });
- });
-
- describe('installation commands', () => {
- it('renders the correct command', () => {
- expect(findCodeInstructions().at(0).props('instruction')).toBe(conanInstallationCommandStr);
- });
- });
-
- describe('setup commands', () => {
- it('renders the correct command', () => {
- expect(findCodeInstructions().at(1).props('instruction')).toBe(conanSetupCommandStr);
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/dependency_row_spec.js b/spec/frontend/packages/details/components/dependency_row_spec.js
deleted file mode 100644
index 7d3ee92908d..00000000000
--- a/spec/frontend/packages/details/components/dependency_row_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import DependencyRow from '~/packages/details/components/dependency_row.vue';
-import { dependencyLinks } from '../../mock_data';
-
-describe('DependencyRow', () => {
- let wrapper;
-
- const { withoutFramework, withoutVersion, fullLink } = dependencyLinks;
-
- function createComponent({ dependencyLink = fullLink } = {}) {
- wrapper = shallowMount(DependencyRow, {
- propsData: {
- dependency: dependencyLink,
- },
- });
- }
-
- const dependencyVersion = () => wrapper.find('[data-testid="version-pattern"]');
- const dependencyFramework = () => wrapper.find('[data-testid="target-framework"]');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('renders', () => {
- it('full dependency', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- describe('version', () => {
- it('does not render any version information when not supplied', () => {
- createComponent({ dependencyLink: withoutVersion });
-
- expect(dependencyVersion().exists()).toBe(false);
- });
-
- it('does render version info when it exists', () => {
- createComponent();
-
- expect(dependencyVersion().exists()).toBe(true);
- expect(dependencyVersion().text()).toBe(fullLink.version_pattern);
- });
- });
-
- describe('target framework', () => {
- it('does not render any framework information when not supplied', () => {
- createComponent({ dependencyLink: withoutFramework });
-
- expect(dependencyFramework().exists()).toBe(false);
- });
-
- it('does render framework info when it exists', () => {
- createComponent();
-
- expect(dependencyFramework().exists()).toBe(true);
- expect(dependencyFramework().text()).toBe(`(${fullLink.target_framework})`);
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/installation_title_spec.js b/spec/frontend/packages/details/components/installation_title_spec.js
deleted file mode 100644
index 14e990d3011..00000000000
--- a/spec/frontend/packages/details/components/installation_title_spec.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import InstallationTitle from '~/packages/details/components/installation_title.vue';
-import PersistedDropdownSelection from '~/vue_shared/components/registry/persisted_dropdown_selection.vue';
-
-describe('InstallationTitle', () => {
- let wrapper;
-
- const defaultProps = { packageType: 'foo', options: [{ value: 'foo', label: 'bar' }] };
-
- const findPersistedDropdownSelection = () => wrapper.findComponent(PersistedDropdownSelection);
- const findTitle = () => wrapper.find('h3');
-
- function createComponent({ props = {} } = {}) {
- wrapper = shallowMount(InstallationTitle, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('has a title', () => {
- createComponent();
-
- expect(findTitle().exists()).toBe(true);
- expect(findTitle().text()).toBe('Installation');
- });
-
- describe('persisted dropdown selection', () => {
- it('exists', () => {
- createComponent();
-
- expect(findPersistedDropdownSelection().exists()).toBe(true);
- });
-
- it('has the correct props', () => {
- createComponent();
-
- expect(findPersistedDropdownSelection().props()).toMatchObject({
- storageKey: 'package_foo_installation_instructions',
- options: defaultProps.options,
- });
- });
-
- it('on change event emits a change event', () => {
- createComponent();
-
- findPersistedDropdownSelection().vm.$emit('change', 'baz');
-
- expect(wrapper.emitted('change')).toEqual([['baz']]);
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/installations_commands_spec.js b/spec/frontend/packages/details/components/installations_commands_spec.js
deleted file mode 100644
index 164f9f69741..00000000000
--- a/spec/frontend/packages/details/components/installations_commands_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
-import ConanInstallation from '~/packages/details/components/conan_installation.vue';
-import InstallationCommands from '~/packages/details/components/installation_commands.vue';
-
-import MavenInstallation from '~/packages/details/components/maven_installation.vue';
-import NpmInstallation from '~/packages/details/components/npm_installation.vue';
-import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
-import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
-import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue';
-
-import {
- conanPackage,
- mavenPackage,
- npmPackage,
- nugetPackage,
- pypiPackage,
- composerPackage,
- terraformModule,
-} from '../../mock_data';
-
-describe('InstallationCommands', () => {
- let wrapper;
-
- function createComponent(propsData) {
- wrapper = shallowMount(InstallationCommands, {
- propsData,
- });
- }
-
- 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 terraformInstallation = () => wrapper.findComponent(TerraformInstallation);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('installation instructions', () => {
- describe.each`
- packageEntity | selector
- ${conanPackage} | ${conanInstallation}
- ${mavenPackage} | ${mavenInstallation}
- ${npmPackage} | ${npmInstallation}
- ${nugetPackage} | ${nugetInstallation}
- ${pypiPackage} | ${pypiInstallation}
- ${composerPackage} | ${composerInstallation}
- ${terraformModule} | ${terraformInstallation}
- `('renders', ({ packageEntity, selector }) => {
- it(`${packageEntity.package_type} instructions exist`, () => {
- createComponent({ packageEntity });
-
- expect(selector()).toExist();
- });
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/maven_installation_spec.js b/spec/frontend/packages/details/components/maven_installation_spec.js
deleted file mode 100644
index 4972fe70a3d..00000000000
--- a/spec/frontend/packages/details/components/maven_installation_spec.js
+++ /dev/null
@@ -1,184 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { registryUrl as mavenPath } from 'jest/packages/details/mock_data';
-import { mavenPackage as packageEntity } from 'jest/packages/mock_data';
-import InstallationTitle from '~/packages/details/components/installation_title.vue';
-import MavenInstallation from '~/packages/details/components/maven_installation.vue';
-import { TrackingActions } from '~/packages/details/constants';
-import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('MavenInstallation', () => {
- let wrapper;
-
- const xmlCodeBlock = 'foo/xml';
- const mavenCommandStr = 'foo/command';
- const mavenSetupXml = 'foo/setup';
- const gradleGroovyInstallCommandText = 'foo/gradle/groovy/install';
- const gradleGroovyAddSourceCommandText = 'foo/gradle/groovy/add/source';
- const gradleKotlinInstallCommandText = 'foo/gradle/kotlin/install';
- const gradleKotlinAddSourceCommandText = 'foo/gradle/kotlin/add/source';
-
- const store = new Vuex.Store({
- state: {
- packageEntity,
- mavenPath,
- },
- getters: {
- mavenInstallationXml: () => xmlCodeBlock,
- mavenInstallationCommand: () => mavenCommandStr,
- mavenSetupXml: () => mavenSetupXml,
- gradleGroovyInstalCommand: () => gradleGroovyInstallCommandText,
- gradleGroovyAddSourceCommand: () => gradleGroovyAddSourceCommandText,
- gradleKotlinInstalCommand: () => gradleKotlinInstallCommandText,
- gradleKotlinAddSourceCommand: () => gradleKotlinAddSourceCommandText,
- },
- });
-
- const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
- const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
-
- function createComponent({ data = {} } = {}) {
- wrapper = shallowMount(MavenInstallation, {
- localVue,
- store,
- data() {
- return data;
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('install command switch', () => {
- it('has the installation title component', () => {
- createComponent();
-
- expect(findInstallationTitle().exists()).toBe(true);
- expect(findInstallationTitle().props()).toMatchObject({
- packageType: 'maven',
- options: [
- { value: 'maven', label: 'Maven XML' },
- { value: 'groovy', label: 'Gradle Groovy DSL' },
- { value: 'kotlin', label: 'Gradle Kotlin DSL' },
- ],
- });
- });
-
- it('on change event updates the instructions to show', async () => {
- createComponent();
-
- expect(findCodeInstructions().at(0).props('instruction')).toBe(xmlCodeBlock);
- findInstallationTitle().vm.$emit('change', 'groovy');
-
- await nextTick();
-
- expect(findCodeInstructions().at(0).props('instruction')).toBe(
- gradleGroovyInstallCommandText,
- );
- });
- });
-
- describe('maven', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders all the messages', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('installation commands', () => {
- it('renders the correct xml block', () => {
- expect(findCodeInstructions().at(0).props()).toMatchObject({
- instruction: xmlCodeBlock,
- multiline: true,
- trackingAction: TrackingActions.COPY_MAVEN_XML,
- });
- });
-
- it('renders the correct maven command', () => {
- expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: mavenCommandStr,
- multiline: false,
- trackingAction: TrackingActions.COPY_MAVEN_COMMAND,
- });
- });
- });
-
- describe('setup commands', () => {
- it('renders the correct xml block', () => {
- expect(findCodeInstructions().at(2).props()).toMatchObject({
- instruction: mavenSetupXml,
- multiline: true,
- trackingAction: TrackingActions.COPY_MAVEN_SETUP,
- });
- });
- });
- });
-
- describe('groovy', () => {
- beforeEach(() => {
- createComponent({ data: { instructionType: 'groovy' } });
- });
-
- it('renders all the messages', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('installation commands', () => {
- it('renders the gradle install command', () => {
- expect(findCodeInstructions().at(0).props()).toMatchObject({
- instruction: gradleGroovyInstallCommandText,
- multiline: false,
- trackingAction: TrackingActions.COPY_GRADLE_INSTALL_COMMAND,
- });
- });
- });
-
- describe('setup commands', () => {
- it('renders the correct gradle command', () => {
- expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: gradleGroovyAddSourceCommandText,
- multiline: true,
- trackingAction: TrackingActions.COPY_GRADLE_ADD_TO_SOURCE_COMMAND,
- });
- });
- });
- });
-
- describe('kotlin', () => {
- beforeEach(() => {
- createComponent({ data: { instructionType: 'kotlin' } });
- });
-
- it('renders all the messages', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('installation commands', () => {
- it('renders the gradle install command', () => {
- expect(findCodeInstructions().at(0).props()).toMatchObject({
- instruction: gradleKotlinInstallCommandText,
- multiline: false,
- trackingAction: TrackingActions.COPY_KOTLIN_INSTALL_COMMAND,
- });
- });
- });
-
- describe('setup commands', () => {
- it('renders the correct gradle command', () => {
- expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: gradleKotlinAddSourceCommandText,
- multiline: true,
- trackingAction: TrackingActions.COPY_KOTLIN_ADD_TO_SOURCE_COMMAND,
- });
- });
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/npm_installation_spec.js b/spec/frontend/packages/details/components/npm_installation_spec.js
deleted file mode 100644
index 1c49110bdf8..00000000000
--- a/spec/frontend/packages/details/components/npm_installation_spec.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
-import { npmPackage as packageEntity } from 'jest/packages/mock_data';
-import InstallationTitle from '~/packages/details/components/installation_title.vue';
-import NpmInstallation from '~/packages/details/components/npm_installation.vue';
-import { TrackingActions } from '~/packages/details/constants';
-import { npmInstallationCommand, npmSetupCommand } from '~/packages/details/store/getters';
-import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('NpmInstallation', () => {
- let wrapper;
-
- const npmInstallationCommandLabel = 'npm i @Test/package';
- const yarnInstallationCommandLabel = 'yarn add @Test/package';
-
- const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
- const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
-
- function createComponent({ data = {} } = {}) {
- const store = new Vuex.Store({
- state: {
- packageEntity,
- nugetPath,
- },
- getters: {
- npmInstallationCommand,
- npmSetupCommand,
- },
- });
-
- wrapper = shallowMount(NpmInstallation, {
- localVue,
- store,
- data() {
- return data;
- },
- });
- }
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders all the messages', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('install command switch', () => {
- it('has the installation title component', () => {
- expect(findInstallationTitle().exists()).toBe(true);
- expect(findInstallationTitle().props()).toMatchObject({
- packageType: 'npm',
- options: [
- { value: 'npm', label: 'Show NPM commands' },
- { value: 'yarn', label: 'Show Yarn commands' },
- ],
- });
- });
-
- it('on change event updates the instructions to show', async () => {
- createComponent();
-
- expect(findCodeInstructions().at(0).props('instruction')).toBe(npmInstallationCommandLabel);
- findInstallationTitle().vm.$emit('change', 'yarn');
-
- await nextTick();
-
- expect(findCodeInstructions().at(0).props('instruction')).toBe(yarnInstallationCommandLabel);
- });
- });
-
- describe('npm', () => {
- beforeEach(() => {
- createComponent();
- });
- it('renders the correct installation command', () => {
- expect(findCodeInstructions().at(0).props()).toMatchObject({
- instruction: npmInstallationCommandLabel,
- multiline: false,
- trackingAction: TrackingActions.COPY_NPM_INSTALL_COMMAND,
- });
- });
-
- it('renders the correct setup command', () => {
- expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo @Test:registry=undefined/ >> .npmrc',
- multiline: false,
- trackingAction: TrackingActions.COPY_NPM_SETUP_COMMAND,
- });
- });
- });
-
- describe('yarn', () => {
- beforeEach(() => {
- createComponent({ data: { instructionType: 'yarn' } });
- });
-
- it('renders the correct setup command', () => {
- expect(findCodeInstructions().at(0).props()).toMatchObject({
- instruction: yarnInstallationCommandLabel,
- multiline: false,
- trackingAction: TrackingActions.COPY_YARN_INSTALL_COMMAND,
- });
- });
-
- it('renders the correct registry command', () => {
- expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: 'echo \\"@Test:registry\\" \\"undefined/\\" >> .yarnrc',
- multiline: false,
- trackingAction: TrackingActions.COPY_YARN_SETUP_COMMAND,
- });
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/nuget_installation_spec.js b/spec/frontend/packages/details/components/nuget_installation_spec.js
deleted file mode 100644
index 8839a8f1108..00000000000
--- a/spec/frontend/packages/details/components/nuget_installation_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
-import { nugetPackage as packageEntity } from 'jest/packages/mock_data';
-import InstallationTitle from '~/packages/details/components/installation_title.vue';
-import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
-import { TrackingActions } from '~/packages/details/constants';
-import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('NugetInstallation', () => {
- let wrapper;
-
- const nugetInstallationCommandStr = 'foo/command';
- const nugetSetupCommandStr = 'foo/setup';
-
- const store = new Vuex.Store({
- state: {
- packageEntity,
- nugetPath,
- },
- getters: {
- nugetInstallationCommand: () => nugetInstallationCommandStr,
- nugetSetupCommand: () => nugetSetupCommandStr,
- },
- });
-
- const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
- const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
-
- function createComponent() {
- wrapper = shallowMount(NugetInstallation, {
- localVue,
- store,
- });
- }
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders all the messages', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('install command switch', () => {
- it('has the installation title component', () => {
- expect(findInstallationTitle().exists()).toBe(true);
- expect(findInstallationTitle().props()).toMatchObject({
- packageType: 'nuget',
- options: [{ value: 'nuget', label: 'Show Nuget commands' }],
- });
- });
- });
-
- describe('installation commands', () => {
- it('renders the correct command', () => {
- expect(findCodeInstructions().at(0).props()).toMatchObject({
- instruction: nugetInstallationCommandStr,
- trackingAction: TrackingActions.COPY_NUGET_INSTALL_COMMAND,
- });
- });
- });
-
- describe('setup commands', () => {
- it('renders the correct command', () => {
- expect(findCodeInstructions().at(1).props()).toMatchObject({
- instruction: nugetSetupCommandStr,
- trackingAction: TrackingActions.COPY_NUGET_SETUP_COMMAND,
- });
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/package_title_spec.js b/spec/frontend/packages/details/components/package_title_spec.js
deleted file mode 100644
index 512cec85b40..00000000000
--- a/spec/frontend/packages/details/components/package_title_spec.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import PackageTitle from '~/packages/details/components/package_title.vue';
-import PackageTags from '~/packages/shared/components/package_tags.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import {
- conanPackage,
- mavenFiles,
- mavenPackage,
- mockTags,
- npmFiles,
- npmPackage,
- nugetPackage,
-} from '../../mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('PackageTitle', () => {
- let wrapper;
- let store;
-
- function createComponent({
- packageEntity = mavenPackage,
- packageFiles = mavenFiles,
- icon = null,
- } = {}) {
- store = new Vuex.Store({
- state: {
- packageEntity,
- packageFiles,
- },
- getters: {
- packageTypeDisplay: ({ packageEntity: { package_type: type } }) => type,
- packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline,
- packageIcon: () => icon,
- },
- });
-
- wrapper = shallowMount(PackageTitle, {
- localVue,
- store,
- stubs: {
- TitleArea,
- },
- });
- return wrapper.vm.$nextTick();
- }
-
- const findTitleArea = () => wrapper.find(TitleArea);
- const packageType = () => wrapper.find('[data-testid="package-type"]');
- const packageSize = () => wrapper.find('[data-testid="package-size"]');
- const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
- const packageRef = () => wrapper.find('[data-testid="package-ref"]');
- const packageTags = () => wrapper.find(PackageTags);
- const packageBadges = () => wrapper.findAll('[data-testid="tag-badge"]');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('renders', () => {
- it('without tags', async () => {
- await createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('with tags', async () => {
- await createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('with tags on mobile', async () => {
- jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false);
- await createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
- await wrapper.vm.$nextTick();
-
- expect(packageBadges()).toHaveLength(mockTags.length);
- });
- });
-
- describe('package title', () => {
- it('is correctly bound', async () => {
- await createComponent();
-
- expect(findTitleArea().props('title')).toBe('Test package');
- });
- });
-
- describe('package icon', () => {
- const fakeSrc = 'a-fake-src';
-
- it('binds an icon when provided one from vuex', async () => {
- await createComponent({ icon: fakeSrc });
-
- expect(findTitleArea().props('avatar')).toBe(fakeSrc);
- });
-
- it('do not binds an icon when not provided one', async () => {
- await createComponent();
-
- expect(findTitleArea().props('avatar')).toBe(null);
- });
- });
-
- describe.each`
- packageEntity | text
- ${conanPackage} | ${'conan'}
- ${mavenPackage} | ${'maven'}
- ${npmPackage} | ${'npm'}
- ${nugetPackage} | ${'nuget'}
- `(`package type`, ({ packageEntity, text }) => {
- beforeEach(() => createComponent({ packageEntity }));
-
- it(`${packageEntity.package_type} should render from Vuex getters ${text}`, () => {
- expect(packageType().props()).toEqual(expect.objectContaining({ text, icon: 'package' }));
- });
- });
-
- describe('calculates the package size', () => {
- it('correctly calculates when there is only 1 file', async () => {
- await createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
-
- expect(packageSize().props()).toMatchObject({ text: '200 bytes', icon: 'disk' });
- });
-
- it('correctly calulates when there are multiple files', async () => {
- await createComponent();
-
- expect(packageSize().props('text')).toBe('300 bytes');
- });
- });
-
- describe('package tags', () => {
- it('displays the package-tags component when the package has tags', async () => {
- await createComponent({
- packageEntity: {
- ...npmPackage,
- tags: mockTags,
- },
- });
-
- expect(packageTags().exists()).toBe(true);
- });
-
- it('does not display the package-tags component when there are no tags', async () => {
- await createComponent();
-
- expect(packageTags().exists()).toBe(false);
- });
- });
-
- describe('package ref', () => {
- it('does not display the ref if missing', async () => {
- await createComponent();
-
- expect(packageRef().exists()).toBe(false);
- });
-
- it('correctly shows the package ref if there is one', async () => {
- await createComponent({ packageEntity: npmPackage });
- expect(packageRef().props()).toMatchObject({
- text: npmPackage.pipeline.ref,
- icon: 'branch',
- });
- });
- });
-
- describe('pipeline project', () => {
- it('does not display the project if missing', async () => {
- await createComponent();
-
- expect(pipelineProject().exists()).toBe(false);
- });
-
- it('correctly shows the pipeline project if there is one', async () => {
- await createComponent({ packageEntity: npmPackage });
-
- expect(pipelineProject().props()).toMatchObject({
- text: npmPackage.pipeline.project.name,
- icon: 'review-list',
- link: npmPackage.pipeline.project.web_url,
- });
- });
- });
-});
diff --git a/spec/frontend/packages/details/components/pypi_installation_spec.js b/spec/frontend/packages/details/components/pypi_installation_spec.js
deleted file mode 100644
index 2cec84282d9..00000000000
--- a/spec/frontend/packages/details/components/pypi_installation_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { pypiPackage as packageEntity } from 'jest/packages/mock_data';
-import InstallationTitle from '~/packages/details/components/installation_title.vue';
-import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('PypiInstallation', () => {
- let wrapper;
-
- const pipCommandStr = 'pip install';
- const pypiSetupStr = 'python setup';
-
- const store = new Vuex.Store({
- state: {
- packageEntity,
- pypiHelpPath: 'foo',
- },
- getters: {
- pypiPipCommand: () => pipCommandStr,
- pypiSetupCommand: () => pypiSetupStr,
- },
- });
-
- const pipCommand = () => wrapper.find('[data-testid="pip-command"]');
- const setupInstruction = () => wrapper.find('[data-testid="pypi-setup-content"]');
-
- const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
-
- function createComponent() {
- wrapper = shallowMount(PypiInstallation, {
- localVue,
- store,
- });
- }
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('install command switch', () => {
- it('has the installation title component', () => {
- expect(findInstallationTitle().exists()).toBe(true);
- expect(findInstallationTitle().props()).toMatchObject({
- packageType: 'pypi',
- options: [{ value: 'pypi', label: 'Show PyPi commands' }],
- });
- });
- });
-
- it('renders all the messages', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('installation commands', () => {
- it('renders the correct pip command', () => {
- expect(pipCommand().props('instruction')).toBe(pipCommandStr);
- });
- });
-
- describe('setup commands', () => {
- it('renders the correct setup block', () => {
- expect(setupInstruction().props('instruction')).toBe(pypiSetupStr);
- });
- });
-});
diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js
deleted file mode 100644
index 8210511bf8f..00000000000
--- a/spec/frontend/packages/details/store/getters_spec.js
+++ /dev/null
@@ -1,295 +0,0 @@
-import { NpmManager } from '~/packages/details/constants';
-import {
- conanInstallationCommand,
- conanSetupCommand,
- packagePipeline,
- packageTypeDisplay,
- packageIcon,
- mavenInstallationXml,
- mavenInstallationCommand,
- mavenSetupXml,
- npmInstallationCommand,
- npmSetupCommand,
- nugetInstallationCommand,
- nugetSetupCommand,
- pypiPipCommand,
- pypiSetupCommand,
- composerRegistryInclude,
- composerPackageInclude,
- groupExists,
- gradleGroovyInstalCommand,
- gradleGroovyAddSourceCommand,
- gradleKotlinInstalCommand,
- gradleKotlinAddSourceCommand,
-} from '~/packages/details/store/getters';
-import {
- conanPackage,
- npmPackage,
- nugetPackage,
- mockPipelineInfo,
- mavenPackage as packageWithoutBuildInfo,
- pypiPackage,
- rubygemsPackage,
-} from '../../mock_data';
-import {
- generateMavenCommand,
- generateXmlCodeBlock,
- generateMavenSetupXml,
- registryUrl,
- pypiSetupCommandStr,
-} from '../mock_data';
-
-describe('Getters PackageDetails Store', () => {
- let state;
-
- const defaultState = {
- packageEntity: packageWithoutBuildInfo,
- conanPath: registryUrl,
- mavenPath: registryUrl,
- npmPath: registryUrl,
- nugetPath: registryUrl,
- pypiPath: registryUrl,
- };
-
- const setupState = (testState = {}) => {
- state = {
- ...defaultState,
- ...testState,
- };
- };
-
- const conanInstallationCommandStr = `conan install ${conanPackage.name} --remote=gitlab`;
- const conanSetupCommandStr = `conan remote add gitlab ${registryUrl}`;
-
- const mavenCommandStr = generateMavenCommand(packageWithoutBuildInfo.maven_metadatum);
- const mavenInstallationXmlBlock = generateXmlCodeBlock(packageWithoutBuildInfo.maven_metadatum);
- const mavenSetupXmlBlock = generateMavenSetupXml();
-
- const npmInstallStr = `npm i ${npmPackage.name}`;
- const npmSetupStr = `echo @Test:registry=${registryUrl}/ >> .npmrc`;
- const yarnInstallStr = `yarn add ${npmPackage.name}`;
- const yarnSetupStr = `echo \\"@Test:registry\\" \\"${registryUrl}/\\" >> .yarnrc`;
-
- const nugetInstallationCommandStr = `nuget install ${nugetPackage.name} -Source "GitLab"`;
- const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`;
-
- const pypiPipCommandStr = `pip install ${pypiPackage.name} --extra-index-url ${registryUrl}`;
- const composerRegistryIncludeStr =
- 'composer config repositories.gitlab.com/123 \'{"type": "composer", "url": "foo"}\'';
- const composerPackageIncludeStr = `composer req ${[packageWithoutBuildInfo.name]}:${
- packageWithoutBuildInfo.version
- }`;
-
- describe('packagePipeline', () => {
- it('should return the pipeline info when pipeline exists', () => {
- setupState({
- packageEntity: {
- ...npmPackage,
- pipeline: mockPipelineInfo,
- },
- });
-
- expect(packagePipeline(state)).toEqual(mockPipelineInfo);
- });
-
- it('should return null when build_info does not exist', () => {
- setupState();
-
- expect(packagePipeline(state)).toBe(null);
- });
- });
-
- describe('packageTypeDisplay', () => {
- describe.each`
- packageEntity | expectedResult
- ${conanPackage} | ${'Conan'}
- ${packageWithoutBuildInfo} | ${'Maven'}
- ${npmPackage} | ${'npm'}
- ${nugetPackage} | ${'NuGet'}
- ${pypiPackage} | ${'PyPI'}
- ${rubygemsPackage} | ${'RubyGems'}
- `(`package type`, ({ packageEntity, expectedResult }) => {
- beforeEach(() => setupState({ packageEntity }));
-
- it(`${packageEntity.package_type} should show as ${expectedResult}`, () => {
- expect(packageTypeDisplay(state)).toBe(expectedResult);
- });
- });
- });
-
- describe('packageIcon', () => {
- describe('nuget packages', () => {
- it('should return nuget package icon', () => {
- setupState({ packageEntity: nugetPackage });
-
- expect(packageIcon(state)).toBe(nugetPackage.nuget_metadatum.icon_url);
- });
-
- it('should return null when nuget package does not have an icon', () => {
- setupState({ packageEntity: { ...nugetPackage, nuget_metadatum: {} } });
-
- expect(packageIcon(state)).toBe(null);
- });
- });
-
- it('should not find icons for other package types', () => {
- setupState({ packageEntity: npmPackage });
-
- expect(packageIcon(state)).toBe(null);
- });
- });
-
- describe('conan string getters', () => {
- it('gets the correct conanInstallationCommand', () => {
- setupState({ packageEntity: conanPackage });
-
- expect(conanInstallationCommand(state)).toBe(conanInstallationCommandStr);
- });
-
- it('gets the correct conanSetupCommand', () => {
- setupState({ packageEntity: conanPackage });
-
- expect(conanSetupCommand(state)).toBe(conanSetupCommandStr);
- });
- });
-
- describe('maven string getters', () => {
- it('gets the correct mavenInstallationXml', () => {
- setupState();
-
- expect(mavenInstallationXml(state)).toBe(mavenInstallationXmlBlock);
- });
-
- it('gets the correct mavenInstallationCommand', () => {
- setupState();
-
- expect(mavenInstallationCommand(state)).toBe(mavenCommandStr);
- });
-
- it('gets the correct mavenSetupXml', () => {
- setupState();
-
- expect(mavenSetupXml(state)).toBe(mavenSetupXmlBlock);
- });
- });
-
- describe('npm string getters', () => {
- it('gets the correct npmInstallationCommand for npm', () => {
- setupState({ packageEntity: npmPackage });
-
- expect(npmInstallationCommand(state)(NpmManager.NPM)).toBe(npmInstallStr);
- });
-
- it('gets the correct npmSetupCommand for npm', () => {
- setupState({ packageEntity: npmPackage });
-
- expect(npmSetupCommand(state)(NpmManager.NPM)).toBe(npmSetupStr);
- });
-
- it('gets the correct npmInstallationCommand for Yarn', () => {
- setupState({ packageEntity: npmPackage });
-
- expect(npmInstallationCommand(state)(NpmManager.YARN)).toBe(yarnInstallStr);
- });
-
- it('gets the correct npmSetupCommand for Yarn', () => {
- setupState({ packageEntity: npmPackage });
-
- expect(npmSetupCommand(state)(NpmManager.YARN)).toBe(yarnSetupStr);
- });
- });
-
- describe('nuget string getters', () => {
- it('gets the correct nugetInstallationCommand', () => {
- setupState({ packageEntity: nugetPackage });
-
- expect(nugetInstallationCommand(state)).toBe(nugetInstallationCommandStr);
- });
-
- it('gets the correct nugetSetupCommand', () => {
- setupState({ packageEntity: nugetPackage });
-
- expect(nugetSetupCommand(state)).toBe(nugetSetupCommandStr);
- });
- });
-
- describe('pypi string getters', () => {
- it('gets the correct pypiPipCommand', () => {
- setupState({ packageEntity: pypiPackage });
-
- expect(pypiPipCommand(state)).toBe(pypiPipCommandStr);
- });
-
- it('gets the correct pypiSetupCommand', () => {
- setupState({ pypiSetupPath: 'foo' });
-
- expect(pypiSetupCommand(state)).toBe(pypiSetupCommandStr);
- });
- });
-
- describe('composer string getters', () => {
- it('gets the correct composerRegistryInclude command', () => {
- setupState({ composerPath: 'foo', composerConfigRepositoryName: 'gitlab.com/123' });
-
- expect(composerRegistryInclude(state)).toBe(composerRegistryIncludeStr);
- });
-
- it('gets the correct composerPackageInclude command', () => {
- setupState();
-
- expect(composerPackageInclude(state)).toBe(composerPackageIncludeStr);
- });
- });
-
- describe('gradle groovy string getters', () => {
- it('gets the correct gradleGroovyInstalCommand', () => {
- setupState();
-
- expect(gradleGroovyInstalCommand(state)).toMatchInlineSnapshot(
- `"implementation 'com.test.app:test-app:1.0-SNAPSHOT'"`,
- );
- });
-
- it('gets the correct gradleGroovyAddSourceCommand', () => {
- setupState();
-
- expect(gradleGroovyAddSourceCommand(state)).toMatchInlineSnapshot(`
- "maven {
- url 'foo/registry'
- }"
- `);
- });
- });
-
- describe('gradle kotlin string getters', () => {
- it('gets the correct gradleKotlinInstalCommand', () => {
- setupState();
-
- expect(gradleKotlinInstalCommand(state)).toMatchInlineSnapshot(
- `"implementation(\\"com.test.app:test-app:1.0-SNAPSHOT\\")"`,
- );
- });
-
- it('gets the correct gradleKotlinAddSourceCommand', () => {
- setupState();
-
- expect(gradleKotlinAddSourceCommand(state)).toMatchInlineSnapshot(
- `"maven(\\"foo/registry\\")"`,
- );
- });
- });
-
- describe('check if group', () => {
- it('is set', () => {
- setupState({ groupListUrl: '/groups/composer/-/packages' });
-
- expect(groupExists(state)).toBe(true);
- });
-
- it('is not set', () => {
- setupState({ groupListUrl: '' });
-
- expect(groupExists(state)).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js
index bd15d48c4eb..5f2fc8ddfbd 100644
--- a/spec/frontend/packages/shared/components/package_list_row_spec.js
+++ b/spec/frontend/packages/shared/components/package_list_row_spec.js
@@ -1,5 +1,5 @@
import { GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
@@ -19,14 +19,14 @@ describe('packages_list_row', () => {
const InfrastructureIconAndName = { name: 'InfrastructureIconAndName', template: '<div></div>' };
const PackageIconAndName = { name: 'PackageIconAndName', template: '<div></div>' };
- const findPackageTags = () => wrapper.find(PackageTags);
- const findPackagePath = () => wrapper.find(PackagePath);
- const findDeleteButton = () => wrapper.find('[data-testid="action-delete"]');
- const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
+ const findPackageTags = () => wrapper.findComponent(PackageTags);
+ const findPackagePath = () => wrapper.findComponent(PackagePath);
+ const findDeleteButton = () => wrapper.findByTestId('action-delete');
+ const findPackageIconAndName = () => wrapper.findComponent(PackageIconAndName);
const findInfrastructureIconAndName = () => wrapper.findComponent(InfrastructureIconAndName);
const findListItem = () => wrapper.findComponent(ListItem);
const findPackageLink = () => wrapper.findComponent(GlLink);
- const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]');
+ const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const mountComponent = ({
isGroup = false,
@@ -35,7 +35,7 @@ describe('packages_list_row', () => {
disableDelete = false,
provide,
} = {}) => {
- wrapper = shallowMount(PackagesListRow, {
+ wrapper = shallowMountExtended(PackagesListRow, {
store,
provide,
stubs: {
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
new file mode 100644
index 00000000000..1f0252965b0
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -0,0 +1,173 @@
+import { GlFormInputGroup, GlFormGroup, GlSkeletonLoader, GlSprintf } 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 waitForPromises from 'helpers/wait_for_promises';
+
+import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
+
+import { proxyDetailsQuery, proxyData } from './mock_data';
+
+const localVue = createLocalVue();
+
+describe('DependencyProxyApp', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const provideDefaults = {
+ groupPath: 'gitlab-org',
+ dependencyProxyAvailable: true,
+ };
+
+ function createComponent({
+ provide = provideDefaults,
+ resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()),
+ } = {}) {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[getDependencyProxyDetailsQuery, resolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(DependencyProxyApp, {
+ localVue,
+ apolloProvider,
+ provide,
+ stubs: {
+ GlFormInputGroup,
+ GlFormGroup,
+ GlSprintf,
+ },
+ });
+ }
+
+ const findProxyNotAvailableAlert = () => wrapper.findByTestId('proxy-not-available');
+ const findProxyDisabledAlert = () => wrapper.findByTestId('proxy-disabled');
+ const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findMainArea = () => wrapper.findByTestId('main-area');
+ const findProxyCountText = () => wrapper.findByTestId('proxy-count');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when the dependency proxy is not available', () => {
+ const createComponentArguments = {
+ provide: { ...provideDefaults, dependencyProxyAvailable: false },
+ };
+
+ it('renders an info alert', () => {
+ createComponent(createComponentArguments);
+
+ expect(findProxyNotAvailableAlert().text()).toBe(
+ DependencyProxyApp.i18n.proxyNotAvailableText,
+ );
+ });
+
+ it('does not render the main area', () => {
+ createComponent(createComponentArguments);
+
+ expect(findMainArea().exists()).toBe(false);
+ });
+
+ it('does not call the graphql endpoint', async () => {
+ const resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
+ createComponent({ ...createComponentArguments, resolver });
+
+ await waitForPromises();
+
+ expect(resolver).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when the dependency proxy is available', () => {
+ describe('when is loading', () => {
+ it('renders the skeleton loader', () => {
+ createComponent();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('does not show the main section', () => {
+ createComponent();
+
+ expect(findMainArea().exists()).toBe(false);
+ });
+
+ it('does not render the info alert', () => {
+ createComponent();
+
+ expect(findProxyNotAvailableAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('when the app is loaded', () => {
+ describe('when the dependency proxy is enabled', () => {
+ beforeEach(() => {
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('does not render the info alert', () => {
+ expect(findProxyNotAvailableAlert().exists()).toBe(false);
+ });
+
+ it('renders the main area', () => {
+ expect(findMainArea().exists()).toBe(true);
+ });
+
+ it('renders a form group with a label', () => {
+ expect(findFormGroup().attributes('label')).toBe(
+ DependencyProxyApp.i18n.proxyImagePrefix,
+ );
+ });
+
+ it('renders a form input group', () => {
+ expect(findFormInputGroup().exists()).toBe(true);
+ expect(findFormInputGroup().props('value')).toBe(proxyData().dependencyProxyImagePrefix);
+ });
+
+ it('form input group has a clipboard button', () => {
+ expect(findClipBoardButton().exists()).toBe(true);
+ expect(findClipBoardButton().props()).toMatchObject({
+ text: proxyData().dependencyProxyImagePrefix,
+ title: DependencyProxyApp.i18n.copyImagePrefixText,
+ });
+ });
+
+ it('from group has a description with proxy count', () => {
+ expect(findProxyCountText().text()).toBe('Contains 2 blobs of images (1024 Bytes)');
+ });
+ });
+ describe('when the dependency proxy is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ resolver: jest
+ .fn()
+ .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } })),
+ });
+ return waitForPromises();
+ });
+
+ it('does not show the main area', () => {
+ expect(findMainArea().exists()).toBe(false);
+ });
+
+ it('does not show the loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('shows a proxy disabled alert', () => {
+ expect(findProxyDisabledAlert().text()).toBe(DependencyProxyApp.i18n.proxyDisabledText);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
new file mode 100644
index 00000000000..23d42e109f9
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
@@ -0,0 +1,21 @@
+export const proxyData = () => ({
+ dependencyProxyBlobCount: 2,
+ dependencyProxyTotalSize: '1024 Bytes',
+ dependencyProxyImagePrefix: 'gdk.test:3000/private-group/dependency_proxy/containers',
+ dependencyProxySetting: { enabled: true, __typename: 'DependencyProxySetting' },
+});
+
+export const proxySettings = (extend = {}) => ({ enabled: true, ...extend });
+
+export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({
+ data: {
+ group: {
+ ...proxyData(),
+ __typename: 'Group',
+ dependencyProxySetting: {
+ ...proxySettings(extendSettings),
+ __typename: 'DependencyProxySetting',
+ },
+ },
+ },
+});
diff --git a/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
index 881d441e116..881d441e116 100644
--- a/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/__snapshots__/terraform_installation_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap
index 03236737572..03236737572 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/__snapshots__/terraform_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap
diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
index 377e7e05f09..c7c10cef504 100644
--- a/spec/frontend/packages/details/components/app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
@@ -5,28 +5,19 @@ import Vuex from 'vuex';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import stubChildren from 'helpers/stub_children';
-import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
-import PackagesApp from '~/packages/details/components/app.vue';
-import DependencyRow from '~/packages/details/components/dependency_row.vue';
-import InstallationCommands from '~/packages/details/components/installation_commands.vue';
-import PackageFiles from '~/packages/details/components/package_files.vue';
-import PackageHistory from '~/packages/details/components/package_history.vue';
-import PackageTitle from '~/packages/details/components/package_title.vue';
-import * as getters from '~/packages/details/store/getters';
+import PackagesApp from '~/packages_and_registries/infrastructure_registry/details/components/app.vue';
+import PackageFiles from '~/packages_and_registries/infrastructure_registry/details/components/package_files.vue';
+import PackageHistory from '~/packages_and_registries/infrastructure_registry/details/components/package_history.vue';
+import * as getters from '~/packages_and_registries/infrastructure_registry/details/store/getters';
import PackageListRow 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 TerraformTitle from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue';
+import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue';
import Tracking from '~/tracking';
-import {
- composerPackage,
- conanPackage,
- mavenPackage,
- mavenFiles,
- npmPackage,
- nugetPackage,
-} from '../../mock_data';
+import { mavenPackage, mavenFiles, npmPackage } from 'jest/packages/mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -73,7 +64,7 @@ describe('PackagesApp', () => {
store,
stubs: {
...stubChildren(PackagesApp),
- PackageTitle: false,
+ TerraformTitle: false,
TitleArea: false,
GlButton: false,
GlModal: false,
@@ -84,23 +75,18 @@ describe('PackagesApp', () => {
});
}
- const packageTitle = () => wrapper.find(PackageTitle);
- const emptyState = () => wrapper.find(GlEmptyState);
+ const packageTitle = () => wrapper.findComponent(TerraformTitle);
+ const emptyState = () => wrapper.findComponent(GlEmptyState);
const deleteButton = () => wrapper.find('.js-delete-button');
const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' });
const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' });
const versionsTab = () => wrapper.find('.js-versions-tab > a');
- const packagesLoader = () => wrapper.find(PackagesListLoader);
- const packagesVersionRows = () => wrapper.findAll(PackageListRow);
+ const packagesLoader = () => wrapper.findComponent(PackagesListLoader);
+ const packagesVersionRows = () => wrapper.findAllComponents(PackageListRow);
const noVersionsMessage = () => wrapper.find('[data-testid="no-versions-message"]');
- const dependenciesTab = () => wrapper.find('.js-dependencies-tab > a');
- const dependenciesCountBadge = () => wrapper.find('[data-testid="dependencies-badge"]');
- const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
- const dependencyRows = () => wrapper.findAll(DependencyRow);
- const findPackageHistory = () => wrapper.find(PackageHistory);
- const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
- const findInstallationCommands = () => wrapper.find(InstallationCommands);
- const findPackageFiles = () => wrapper.find(PackageFiles);
+ const findPackageHistory = () => wrapper.findComponent(PackageHistory);
+ const findTerraformInstallation = () => wrapper.findComponent(TerraformInstallation);
+ const findPackageFiles = () => wrapper.findComponent(PackageFiles);
afterEach(() => {
wrapper.destroy();
@@ -129,21 +115,10 @@ describe('PackagesApp', () => {
expect(findPackageHistory().props('projectName')).toEqual(wrapper.vm.projectName);
});
- it('additional metadata has the right props', () => {
+ it('terraform installation exists', () => {
createComponent();
- expect(findAdditionalMetadata().exists()).toBe(true);
- expect(findAdditionalMetadata().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
- });
-
- it('installation commands has the right props', () => {
- createComponent();
- expect(findInstallationCommands().exists()).toBe(true);
- expect(findInstallationCommands().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
- });
- it('hides the files table if package type is COMPOSER', () => {
- createComponent({ packageEntity: composerPackage });
- expect(findPackageFiles().exists()).toBe(false);
+ expect(findTerraformInstallation().exists()).toBe(true);
});
describe('deleting packages', () => {
@@ -198,45 +173,6 @@ describe('PackagesApp', () => {
});
});
- describe('dependency links', () => {
- it('does not show the dependency links for a non nuget package', () => {
- createComponent();
-
- expect(dependenciesTab().exists()).toBe(false);
- });
-
- it('shows the dependencies tab with 0 count when a nuget package with no dependencies', () => {
- createComponent({
- packageEntity: {
- ...nugetPackage,
- dependency_links: [],
- },
- });
-
- return wrapper.vm.$nextTick(() => {
- const dependenciesBadge = dependenciesCountBadge();
-
- expect(dependenciesTab().exists()).toBe(true);
- expect(dependenciesBadge.exists()).toBe(true);
- expect(dependenciesBadge.text()).toBe('0');
- expect(noDependenciesMessage().exists()).toBe(true);
- });
- });
-
- it('renders the correct number of dependency rows for a nuget package', () => {
- createComponent({ packageEntity: nugetPackage });
-
- return wrapper.vm.$nextTick(() => {
- const dependenciesBadge = dependenciesCountBadge();
-
- expect(dependenciesTab().exists()).toBe(true);
- expect(dependenciesBadge.exists()).toBe(true);
- expect(dependenciesBadge.text()).toBe(nugetPackage.dependency_links.length.toString());
- expect(dependencyRows()).toHaveLength(nugetPackage.dependency_links.length);
- });
- });
- });
-
describe('tracking and delete', () => {
describe('delete package', () => {
const originalReferrer = document.referrer;
@@ -305,9 +241,9 @@ describe('PackagesApp', () => {
});
it('tracking category calls packageTypeToTrackCategory', () => {
- createComponent({ packageEntity: conanPackage });
+ createComponent({ packageEntity: npmPackage });
expect(wrapper.vm.tracking.category).toBe(category);
- expect(utilSpy).toHaveBeenCalledWith('conan');
+ expect(utilSpy).toHaveBeenCalledWith('npm');
});
it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => {
@@ -371,7 +307,7 @@ describe('PackagesApp', () => {
});
it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
- createComponent({ packageEntity: conanPackage });
+ createComponent({ packageEntity: npmPackage });
findPackageFiles().vm.$emit('download-file');
expect(eventSpy).toHaveBeenCalledWith(
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
index 87e0059344c..a012ec4ab05 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
@@ -1,7 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { terraformModule, mavenFiles, npmPackage } from 'jest/packages/mock_data';
-import component from '~/packages_and_registries/infrastructure_registry/components/details_title.vue';
+import component from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
const localVue = createLocalVue();
diff --git a/spec/frontend/packages/details/components/file_sha_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
index 7bfcf78baab..9ce590bfb51 100644
--- a/spec/frontend/packages/details/components/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import FileSha from '~/packages/details/components/file_sha.vue';
+import FileSha from '~/packages_and_registries/infrastructure_registry/details/components/file_sha.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
diff --git a/spec/frontend/packages/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
index e8e5a24d3a3..0c5aa30223b 100644
--- a/spec/frontend/packages/details/components/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
@@ -2,11 +2,11 @@ import { GlDropdown, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue/';
import stubChildren from 'helpers/stub_children';
-import component from '~/packages/details/components/package_files.vue';
+import component from '~/packages_and_registries/infrastructure_registry/details/components/package_files.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { npmFiles, mavenFiles } from '../../mock_data';
+import { npmFiles, mavenFiles } from 'jest/packages/mock_data';
describe('Package Files', () => {
let wrapper;
diff --git a/spec/frontend/packages/details/components/package_history_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
index 244805a9c82..4987af9f5b0 100644
--- a/spec/frontend/packages/details/components/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
@@ -1,12 +1,12 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import component from '~/packages/details/components/package_history.vue';
-import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants';
+import component from '~/packages_and_registries/infrastructure_registry/details/components/package_history.vue';
+import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { mavenPackage, mockPipelineInfo } from '../../mock_data';
+import { mavenPackage, mockPipelineInfo } from 'jest/packages/mock_data';
describe('Package History', () => {
let wrapper;
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/terraform_installation_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
index ee1548ed5eb..c26784a4b75 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/terraform_installation_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
@@ -1,7 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { terraformModule as packageEntity } from 'jest/packages/mock_data';
-import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue';
+import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
const localVue = createLocalVue();
diff --git a/spec/frontend/packages/details/mock_data.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/mock_data.js
index d43abcedb2e..d43abcedb2e 100644
--- a/spec/frontend/packages/details/mock_data.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/mock_data.js
diff --git a/spec/frontend/packages/details/store/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
index b16e50debc4..61fa69c2f7a 100644
--- a/spec/frontend/packages/details/store/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
@@ -1,19 +1,19 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import createFlash from '~/flash';
-import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
+import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages_and_registries/infrastructure_registry/details/constants';
import {
fetchPackageVersions,
deletePackage,
deletePackageFile,
-} from '~/packages/details/store/actions';
-import * as types from '~/packages/details/store/mutation_types';
+} from '~/packages_and_registries/infrastructure_registry/details/store/actions';
+import * as types from '~/packages_and_registries/infrastructure_registry/details/store/mutation_types';
import {
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
} from '~/packages/shared/constants';
-import { npmPackage as packageEntity } from '../../mock_data';
+import { npmPackage as packageEntity } from '../../../../../packages/mock_data';
jest.mock('~/flash.js');
jest.mock('~/api.js');
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js
new file mode 100644
index 00000000000..8740691a8ee
--- /dev/null
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/getters_spec.js
@@ -0,0 +1,40 @@
+import { packagePipeline } from '~/packages_and_registries/infrastructure_registry/details/store/getters';
+import {
+ npmPackage,
+ mockPipelineInfo,
+ mavenPackage as packageWithoutBuildInfo,
+} from 'jest/packages/mock_data';
+
+describe('Getters PackageDetails Store', () => {
+ let state;
+
+ const defaultState = {
+ packageEntity: packageWithoutBuildInfo,
+ };
+
+ const setupState = (testState = {}) => {
+ state = {
+ ...defaultState,
+ ...testState,
+ };
+ };
+
+ describe('packagePipeline', () => {
+ it('should return the pipeline info when pipeline exists', () => {
+ setupState({
+ packageEntity: {
+ ...npmPackage,
+ pipeline: mockPipelineInfo,
+ },
+ });
+
+ expect(packagePipeline(state)).toEqual(mockPipelineInfo);
+ });
+
+ it('should return null when build_info does not exist', () => {
+ setupState({ pipeline: undefined });
+
+ expect(packagePipeline(state)).toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/store/mutations_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/mutations_spec.js
index 296ed02d786..6efefea4a14 100644
--- a/spec/frontend/packages/details/store/mutations_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import * as types from '~/packages/details/store/mutation_types';
-import mutations from '~/packages/details/store/mutations';
-import { npmPackage as packageEntity } from '../../mock_data';
+import * as types from '~/packages_and_registries/infrastructure_registry/details/store/mutation_types';
+import mutations from '~/packages_and_registries/infrastructure_registry/details/store/mutations';
+import { npmPackage as packageEntity } from 'jest/packages/mock_data';
describe('Mutations package details Store', () => {
let mockState;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
index 6a7f14dc33f..d5649e39561 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
@@ -21,6 +21,15 @@ exports[`NpmInstallation renders all the messages 1`] = `
Registry setup
</h3>
+ <gl-form-radio-group-stub
+ checked="instance"
+ disabledfield="disabled"
+ htmlfield="html"
+ options="[object Object],[object Object]"
+ textfield="text"
+ valuefield="value"
+ />
+
<code-instruction-stub
copytext="Copy npm setup command"
instruction="echo @gitlab-org:registry=npmPath/ >> .npmrc"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
index 279900edff2..f759fe7a81c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
@@ -9,9 +9,8 @@ import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/c
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
-const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
-
describe('Nuget Metadata', () => {
+ let nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
let wrapper;
const mountComponent = () => {
@@ -52,4 +51,30 @@ describe('Nuget Metadata', () => {
expect(element.props('icon')).toBe(icon);
expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]);
});
+
+ describe('without source', () => {
+ beforeAll(() => {
+ nugetPackage = {
+ packageType: PACKAGE_TYPE_NUGET,
+ metadata: { iconUrl: 'iconUrl', licenseUrl: 'licenseUrl' },
+ };
+ });
+
+ it('does not show additional metadata', () => {
+ expect(findNugetSource().exists()).toBe(false);
+ });
+ });
+
+ describe('without license', () => {
+ beforeAll(() => {
+ nugetPackage = {
+ packageType: PACKAGE_TYPE_NUGET,
+ metadata: { iconUrl: 'iconUrl', projectUrl: 'projectUrl' },
+ };
+ });
+
+ it('does not show additional metadata', () => {
+ expect(findNugetLicense().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
index 083c6858ad0..b89410ede13 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
@@ -1,3 +1,4 @@
+import { GlFormRadioGroup } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -12,6 +13,8 @@ import {
PACKAGE_TYPE_NPM,
NPM_PACKAGE_MANAGER,
YARN_PACKAGE_MANAGER,
+ PROJECT_PACKAGE_ENDPOINT_TYPE,
+ INSTANCE_PACKAGE_ENDPOINT_TYPE,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
@@ -25,12 +28,14 @@ describe('NpmInstallation', () => {
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
+ const findEndPointTypeSector = () => wrapper.findComponent(GlFormRadioGroup);
function createComponent({ data = {} } = {}) {
wrapper = shallowMountExtended(NpmInstallation, {
provide: {
npmHelpPath: 'npmHelpPath',
npmPath: 'npmPath',
+ npmProjectPath: 'npmProjectPath',
},
propsData: {
packageEntity,
@@ -53,6 +58,19 @@ describe('NpmInstallation', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ describe('endpoint type selector', () => {
+ it('has the endpoint type selector', () => {
+ expect(findEndPointTypeSector().exists()).toBe(true);
+ expect(findEndPointTypeSector().vm.$attrs.checked).toBe(INSTANCE_PACKAGE_ENDPOINT_TYPE);
+ expect(findEndPointTypeSector().props()).toMatchObject({
+ options: [
+ { value: INSTANCE_PACKAGE_ENDPOINT_TYPE, text: 'Instance-level' },
+ { value: PROJECT_PACKAGE_ENDPOINT_TYPE, text: 'Project-level' },
+ ],
+ });
+ });
+ });
+
describe('install command switch', () => {
it('has the installation title component', () => {
expect(findInstallationTitle().exists()).toBe(true);
@@ -96,6 +114,28 @@ describe('NpmInstallation', () => {
trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
});
});
+
+ it('renders the correct setup command for different endpoint types', async () => {
+ findEndPointTypeSector().vm.$emit('change', PROJECT_PACKAGE_ENDPOINT_TYPE);
+
+ await nextTick();
+
+ expect(findCodeInstructions().at(1).props()).toMatchObject({
+ instruction: `echo @gitlab-org:registry=npmProjectPath/ >> .npmrc`,
+ multiline: false,
+ trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
+ });
+
+ findEndPointTypeSector().vm.$emit('change', INSTANCE_PACKAGE_ENDPOINT_TYPE);
+
+ await nextTick();
+
+ expect(findCodeInstructions().at(1).props()).toMatchObject({
+ instruction: `echo @gitlab-org:registry=npmPath/ >> .npmrc`,
+ multiline: false,
+ trackingAction: TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
+ });
+ });
});
describe('yarn', () => {
@@ -118,5 +158,27 @@ describe('NpmInstallation', () => {
trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
});
});
+
+ it('renders the correct setup command for different endpoint types', async () => {
+ findEndPointTypeSector().vm.$emit('change', PROJECT_PACKAGE_ENDPOINT_TYPE);
+
+ await nextTick();
+
+ expect(findCodeInstructions().at(1).props()).toMatchObject({
+ instruction: `echo \\"@gitlab-org:registry\\" \\"npmProjectPath/\\" >> .yarnrc`,
+ multiline: false,
+ trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
+ });
+
+ findEndPointTypeSector().vm.$emit('change', INSTANCE_PACKAGE_ENDPOINT_TYPE);
+
+ await nextTick();
+
+ expect(findCodeInstructions().at(1).props()).toMatchObject({
+ instruction: 'echo \\"@gitlab-org:registry\\" \\"npmPath/\\" >> .yarnrc',
+ multiline: false,
+ trackingAction: TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index b69008f04f0..57b8be40a7c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -5,7 +5,7 @@ import {
packageData,
packagePipelines,
} from 'jest/packages_and_registries/package_registry/mock_data';
-import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants';
+import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants';
import component from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
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
new file mode 100644
index 00000000000..1b556be5873
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PackagesListApp renders 1`] = `
+<div>
+ <package-title-stub
+ count="2"
+ helpurl="packageHelpUrl"
+ />
+
+ <package-search-stub />
+</div>
+`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
new file mode 100644
index 00000000000..2f2be797251
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -0,0 +1,122 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`packages_list_row renders 1`] = `
+<div
+ class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
+ data-qa-selector="package_row"
+>
+ <div
+ class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5"
+ >
+ <!---->
+
+ <div
+ class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
+ >
+ <gl-link-stub
+ class="gl-text-body gl-min-w-0"
+ data-qa-selector="package_link"
+ href="http://gdk.test:3000/gitlab-org/gitlab-test/-/packages/111"
+ >
+ <gl-truncate-stub
+ position="end"
+ text="@gitlab-org/package-15"
+ />
+ </gl-link-stub>
+
+ <!---->
+
+ <!---->
+ </div>
+
+ <!---->
+ </div>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
+ >
+ <div
+ class="gl-display-flex"
+ data-testid="left-secondary-infos"
+ >
+ <span>
+ 1.0.0
+ </span>
+
+ <!---->
+
+ <package-icon-and-name-stub>
+
+ npm
+
+ </package-icon-and-name-stub>
+
+ <!---->
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
+ >
+ <publish-method-stub />
+ </div>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-min-h-6"
+ >
+ <span>
+ Created
+ <timeago-tooltip-stub
+ cssclass=""
+ time="2020-08-17T14:23:32Z"
+ tooltipplacement="top"
+ />
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
+ >
+ <gl-button-stub
+ aria-label="Remove package"
+ buttontextclasses=""
+ category="secondary"
+ data-testid="action-delete"
+ icon="remove"
+ size="medium"
+ title="Remove package"
+ variant="danger"
+ />
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex"
+ >
+ <div
+ class="gl-w-7"
+ />
+
+ <!---->
+
+ <div
+ class="gl-w-9"
+ />
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap
deleted file mode 100644
index dbebdeeb452..00000000000
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap
+++ /dev/null
@@ -1,68 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`packages_list_app renders 1`] = `
-<div>
- <div
- help-url="foo"
- />
-
- <div />
-
- <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="helpSvg"
- />
- </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="helpUrl"
- 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/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
new file mode 100644
index 00000000000..919dbe25ffe
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`publish_method renders 1`] = `
+<div
+ class="gl-display-flex gl-align-items-center"
+>
+ <gl-icon-stub
+ class="gl-mr-2"
+ name="git-merge"
+ size="16"
+ />
+
+ <span
+ class="gl-mr-2"
+ data-testid="pipeline-ref"
+ >
+ master
+ </span>
+
+ <gl-icon-stub
+ class="gl-mr-2"
+ name="commit"
+ size="16"
+ />
+
+ <gl-link-stub
+ class="gl-mr-2"
+ data-testid="pipeline-sha"
+ href="/namespace14/project14/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0"
+ >
+ b83d6e39
+ </gl-link-stub>
+
+ <clipboard-button-stub
+ category="tertiary"
+ size="small"
+ text="b83d6e391c22777fca1ed3012fce84f633d7fed0"
+ title="Copy commit SHA"
+ tooltipplacement="top"
+ />
+</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
new file mode 100644
index 00000000000..3958cdf21bb
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
@@ -0,0 +1,154 @@
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import 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 {
+ PROJECT_RESOURCE_TYPE,
+ GROUP_RESOURCE_TYPE,
+ LIST_QUERY_DEBOUNCE_TIME,
+} 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';
+
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+
+describe('PackagesListApp', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const defaultProvide = {
+ packageHelpUrl: 'packageHelpUrl',
+ emptyListIllustration: 'emptyListIllustration',
+ emptyListHelpUrl: 'emptyListHelpUrl',
+ isGroupPage: true,
+ fullPath: 'gitlab-org',
+ };
+
+ const PackageList = {
+ name: 'package-list',
+ template: '<div><slot name="empty-state"></slot></div>',
+ };
+ const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
+
+ const findPackageTitle = () => wrapper.findComponent(PackageTitle);
+ const findSearch = () => wrapper.findComponent(PackageSearch);
+
+ const mountComponent = ({
+ resolver = jest.fn().mockResolvedValue(packagesListQuery()),
+ provide = defaultProvide,
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[getPackagesQuery, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(PackageListApp, {
+ localVue,
+ apolloProvider,
+ provide,
+ stubs: {
+ GlEmptyState,
+ GlLoadingIcon,
+ PackageList,
+ GlSprintf,
+ GlLink,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const waitForDebouncedApollo = () => {
+ jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+ return waitForPromises();
+ };
+
+ it('renders', async () => {
+ mountComponent();
+
+ await waitForDebouncedApollo();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a package title', async () => {
+ mountComponent();
+
+ await waitForDebouncedApollo();
+
+ expect(findPackageTitle().exists()).toBe(true);
+ expect(findPackageTitle().props('count')).toBe(2);
+ });
+
+ describe('search component', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findSearch().exists()).toBe(true);
+ });
+
+ it('on update triggers a new query with updated values', async () => {
+ const resolver = jest.fn().mockResolvedValue(packagesListQuery());
+ mountComponent({ resolver });
+
+ const payload = {
+ sort: 'VERSION_DESC',
+ filters: { packageName: 'foo', packageType: 'CONAN' },
+ };
+
+ findSearch().vm.$emit('update', payload);
+
+ await waitForDebouncedApollo();
+ jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({
+ groupSort: payload.sort,
+ ...payload.filters,
+ }),
+ );
+ });
+ });
+
+ describe.each`
+ type | sortType
+ ${PROJECT_RESOURCE_TYPE} | ${'sort'}
+ ${GROUP_RESOURCE_TYPE} | ${'groupSort'}
+ `('$type query', ({ type, sortType }) => {
+ let provide;
+ let resolver;
+
+ const isGroupPage = type === GROUP_RESOURCE_TYPE;
+
+ beforeEach(() => {
+ provide = { ...defaultProvide, isGroupPage };
+ resolver = jest.fn().mockResolvedValue(packagesListQuery(type));
+ mountComponent({ provide, resolver });
+ return waitForDebouncedApollo();
+ });
+
+ it('succeeds', () => {
+ expect(findPackageTitle().props('count')).toBe(2);
+ });
+
+ it('calls the resolver with the right parameters', () => {
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ isGroupPage, [sortType]: '' }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
new file mode 100644
index 00000000000..a276db104d7
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -0,0 +1,156 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
+import PackagePath from '~/packages/shared/components/package_path.vue';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
+import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue';
+import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants';
+
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
+
+describe('packages_list_row', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ isGroupPage: false,
+ };
+
+ const packageWithoutTags = { ...packageData(), project: packageProject() };
+ const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
+
+ const findPackageTags = () => wrapper.find(PackageTags);
+ const findPackagePath = () => wrapper.find(PackagePath);
+ const findDeleteButton = () => wrapper.findByTestId('action-delete');
+ const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
+ const findListItem = () => wrapper.findComponent(ListItem);
+ const findPackageLink = () => wrapper.findComponent(GlLink);
+ const findWarningIcon = () => wrapper.findByTestId('warning-icon');
+ const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
+
+ const mountComponent = ({
+ packageEntity = packageWithoutTags,
+ provide = defaultProvide,
+ } = {}) => {
+ wrapper = shallowMountExtended(PackagesListRow, {
+ provide,
+ stubs: {
+ ListItem,
+ GlSprintf,
+ },
+ propsData: {
+ packageEntity,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders', () => {
+ mountComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('tags', () => {
+ it('renders package tags when a package has tags', () => {
+ mountComponent({ packageEntity: packageWithTags });
+
+ expect(findPackageTags().exists()).toBe(true);
+ });
+
+ it('does not render when there are no tags', () => {
+ mountComponent();
+
+ expect(findPackageTags().exists()).toBe(false);
+ });
+ });
+
+ describe('when it is group', () => {
+ it('has a package path component', () => {
+ mountComponent({ provide: { isGroupPage: true } });
+
+ expect(findPackagePath().exists()).toBe(true);
+ expect(findPackagePath().props()).toMatchObject({ path: 'gitlab-org/gitlab-test' });
+ });
+ });
+
+ describe('delete button', () => {
+ it('exists and has the correct props', () => {
+ mountComponent({ packageEntity: packageWithoutTags });
+
+ expect(findDeleteButton().exists()).toBe(true);
+ expect(findDeleteButton().attributes()).toMatchObject({
+ icon: 'remove',
+ category: 'secondary',
+ variant: 'danger',
+ title: 'Remove package',
+ });
+ });
+
+ it('emits the packageToDelete event when the delete button is clicked', async () => {
+ mountComponent({ packageEntity: packageWithoutTags });
+
+ findDeleteButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+ expect(wrapper.emitted('packageToDelete')).toBeTruthy();
+ expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
+ });
+ });
+
+ describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
+ beforeEach(() => {
+ mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } });
+ });
+
+ it('list item has a disabled prop', () => {
+ expect(findListItem().props('disabled')).toBe(true);
+ });
+
+ it('details link is disabled', () => {
+ expect(findPackageLink().attributes('disabled')).toBe('true');
+ });
+
+ it('has a warning icon', () => {
+ const icon = findWarningIcon();
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+ expect(icon.props('icon')).toBe('warning');
+ expect(tooltip.value).toMatchObject({
+ title: 'Invalid Package: failed metadata extraction',
+ });
+ });
+
+ it('delete button does not exist', () => {
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+ });
+
+ describe('secondary left info', () => {
+ it('has the package version', () => {
+ mountComponent();
+
+ expect(findLeftSecondaryInfos().text()).toContain(packageWithoutTags.version);
+ });
+
+ it('if the pipeline exists show the author message', () => {
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
+ });
+
+ expect(findLeftSecondaryInfos().text()).toContain('published by Administrator');
+ });
+
+ it('has icon and name component', () => {
+ mountComponent();
+
+ expect(findPackageIconAndName().text()).toBe(packageWithoutTags.packageType.toLowerCase());
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js
deleted file mode 100644
index 6c871a34d50..00000000000
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js
+++ /dev/null
@@ -1,273 +0,0 @@
-import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import createFlash from '~/flash';
-import * as commonUtils from '~/lib/utils/common_utils';
-import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
-import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
-import PackageListApp from '~/packages_and_registries/package_registry/components/list/packages_list_app.vue';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
-import * as packageUtils from '~/packages_and_registries/shared/utils';
-
-jest.mock('~/lib/utils/common_utils');
-jest.mock('~/flash');
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('packages_list_app', () => {
- let wrapper;
- let store;
-
- const PackageList = {
- name: 'package-list',
- template: '<div><slot name="empty-state"></slot></div>',
- };
- 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 = []) => {
- store = new Vuex.Store({
- state: {
- isLoading: false,
- config: {
- resourceId: 'project_id',
- emptyListIllustration: 'helpSvg',
- emptyListHelpUrl,
- packageHelpUrl: 'foo',
- },
- filter,
- },
- });
- store.dispatch = jest.fn();
- };
-
- const mountComponent = (provide) => {
- wrapper = shallowMount(PackageListApp, {
- localVue,
- store,
- stubs: {
- GlEmptyState,
- GlLoadingIcon,
- PackageList,
- GlSprintf,
- GlLink,
- PackageSearch,
- PackageTitle,
- InfrastructureTitle,
- InfrastructureSearch,
- },
- provide,
- });
- };
-
- beforeEach(() => {
- createStore();
- jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders', () => {
- mountComponent();
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('call requestPackagesList on page:changed', () => {
- mountComponent();
- store.dispatch.mockClear();
-
- const list = findListComponent();
- list.vm.$emit('page:changed', 1);
- expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
- });
-
- it('call requestDeletePackage on package:delete', () => {
- mountComponent();
-
- const list = findListComponent();
- list.vm.$emit('package:delete', 'foo');
- expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
- });
-
- it('does call requestPackagesList only one time on render', () => {
- mountComponent();
-
- expect(store.dispatch).toHaveBeenCalledTimes(3);
- expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object));
- expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array));
- expect(store.dispatch).toHaveBeenNthCalledWith(3, 'requestPackagesList');
- });
-
- describe('url query string handling', () => {
- const defaultQueryParamsMock = {
- search: [1, 2],
- type: 'npm',
- sort: 'asc',
- orderBy: 'created',
- };
-
- it('calls setSorting with the query string based sorting', () => {
- jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
-
- mountComponent();
-
- expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', {
- orderBy: defaultQueryParamsMock.orderBy,
- sort: defaultQueryParamsMock.sort,
- });
- });
-
- it('calls setFilter with the query string based filters', () => {
- jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
-
- mountComponent();
-
- expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
- { type: 'type', value: { data: defaultQueryParamsMock.type } },
- { type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[0] } },
- { type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[1] } },
- ]);
- });
-
- it('calls setSorting and setFilters with the results of extractFilterAndSorting', () => {
- jest
- .spyOn(packageUtils, 'extractFilterAndSorting')
- .mockReturnValue({ filters: ['foo'], sorting: { sort: 'desc' } });
-
- mountComponent();
-
- expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { sort: 'desc' });
- expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', ['foo']);
- });
- });
-
- describe('empty state', () => {
- it('generate the correct empty list link', () => {
- mountComponent();
-
- const link = findListComponent().find(GlLink);
-
- expect(link.attributes('href')).toBe(emptyListHelpUrl);
- expect(link.text()).toBe('publish and share your packages');
- });
-
- it('includes the right content on the default tab', () => {
- mountComponent();
-
- const heading = findEmptyState().find('h1');
-
- expect(heading.text()).toBe('There are no packages yet');
- });
- });
-
- describe('filter without results', () => {
- beforeEach(() => {
- createStore([{ type: 'something' }]);
- mountComponent();
- });
-
- it('should show specific empty message', () => {
- expect(findEmptyState().text()).toContain('Sorry, your filter produced no results');
- expect(findEmptyState().text()).toContain(
- 'To widen your search, change or remove the filters above',
- );
- });
- });
-
- describe('Package Search', () => {
- it('exists', () => {
- mountComponent();
-
- expect(findPackageSearch().exists()).toBe(true);
- });
-
- it('on update fetches data from the store', () => {
- mountComponent();
- store.dispatch.mockClear();
-
- findPackageSearch().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`;
-
- beforeEach(() => {
- createStore();
- jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
- setWindowLocation(search);
- });
-
- afterEach(() => {
- setWindowLocation(originalLocation);
- });
-
- it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
- mountComponent();
-
- expect(createFlash).toHaveBeenCalledWith({
- message: DELETE_PACKAGE_SUCCESS_MESSAGE,
- type: 'notice',
- });
- });
-
- it('calls historyReplaceState with a clean url', () => {
- mountComponent();
-
- expect(commonUtils.historyReplaceState).toHaveBeenCalledWith(originalLocation);
- });
-
- it(`does nothing if the query string does not contain ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
- setWindowLocation('?');
- mountComponent();
-
- expect(createFlash).not.toHaveBeenCalled();
- expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
- });
- });
-});
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 42bc9fa3a9e..e65b2a6f320 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,79 +1,79 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sortableFields } from '~/packages/list/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';
import UrlSync from '~/vue_shared/components/url_sync.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+jest.mock('~/packages_and_registries/shared/utils');
+
+useMockLocationHelper();
describe('Package Search', () => {
let wrapper;
- let store;
+
+ const defaultQueryParamsMock = {
+ filters: ['foo'],
+ sorting: { sort: 'desc' },
+ };
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,
+ wrapper = shallowMountExtended(component, {
+ provide() {
+ return {
+ isGroupPage,
+ };
+ },
stubs: {
UrlSync,
},
});
};
+ beforeEach(() => {
+ extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
+ });
+
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- it('has a registry search component', () => {
+ it('has a registry search component', async () => {
mountComponent();
+ await nextTick();
+
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('registry search is mounted after mount', async () => {
+ mountComponent();
+
+ expect(findRegistrySearch().exists()).toBe(false);
+ });
+
+ it('has a UrlSync component', () => {
+ mountComponent();
+
+ expect(findUrlSync().exists()).toBe(true);
});
it.each`
isGroupPage | page
${false} | ${'project'}
${true} | ${'group'}
- `('in a $page page binds the right props', ({ isGroupPage }) => {
+ `('in a $page page binds the right props', async ({ isGroupPage }) => {
mountComponent(isGroupPage);
+ await nextTick();
+
expect(findRegistrySearch().props()).toMatchObject({
- filter: store.state.filter,
- sorting: store.state.sorting,
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
@@ -81,48 +81,85 @@ describe('Package Search', () => {
});
});
- it('on sorting:changed emits update event and calls vuex setSorting', () => {
+ it('on sorting:changed emits update event and update internal sort', async () => {
const payload = { sort: 'foo' };
mountComponent();
+ await nextTick();
+
findRegistrySearch().vm.$emit('sorting:changed', payload);
- expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
- expect(wrapper.emitted('update')).toEqual([[]]);
+ await nextTick();
+
+ expect(findRegistrySearch().props('sorting')).toEqual({ sort: 'foo', orderBy: 'name' });
+
+ // there is always a first call on mounted that emits up default values
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: {
+ packageName: '',
+ packageType: undefined,
+ },
+ sort: 'NAME_FOO',
+ },
+ ]);
});
- it('on filter:changed calls vuex setFilter', () => {
+ it('on filter:changed updates the filters', async () => {
const payload = ['foo'];
mountComponent();
+ await nextTick();
+
findRegistrySearch().vm.$emit('filter:changed', payload);
- expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
+ await nextTick();
+
+ expect(findRegistrySearch().props('filter')).toEqual(['foo']);
});
- it('on filter:submit emits update event', () => {
+ it('on filter:submit emits update event', async () => {
mountComponent();
- findRegistrySearch().vm.$emit('filter:submit');
-
- expect(wrapper.emitted('update')).toEqual([[]]);
- });
+ await nextTick();
- it('has a UrlSync component', () => {
- mountComponent();
+ findRegistrySearch().vm.$emit('filter:submit');
- expect(findUrlSync().exists()).toBe(true);
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: {
+ packageName: '',
+ packageType: undefined,
+ },
+ sort: 'NAME_DESC',
+ },
+ ]);
});
- it('on query:changed calls updateQuery from UrlSync', () => {
+ it('on query:changed calls updateQuery from UrlSync', async () => {
jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
mountComponent();
+ await nextTick();
+
findRegistrySearch().vm.$emit('query:changed');
expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
});
+
+ it('sets the component sorting and filtering based on the querystring', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(getQueryParams).toHaveBeenCalled();
+
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: defaultQueryParamsMock.filters,
+ sorting: defaultQueryParamsMock.sorting,
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
new file mode 100644
index 00000000000..fcbd7cc6a50
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
@@ -0,0 +1,47 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue';
+import { packagePipelines } from '../../mock_data';
+
+const [pipelineData] = packagePipelines();
+
+describe('publish_method', () => {
+ let wrapper;
+
+ const findPipelineRef = () => wrapper.findByTestId('pipeline-ref');
+ const findPipelineSha = () => wrapper.findByTestId('pipeline-sha');
+ const findManualPublish = () => wrapper.findByTestId('manually-published');
+
+ const mountComponent = (pipeline = pipelineData) => {
+ wrapper = shallowMountExtended(PublishMethod, {
+ propsData: {
+ pipeline,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders', () => {
+ mountComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('pipeline information', () => {
+ it('displays branch and commit when pipeline info exists', () => {
+ mountComponent();
+
+ expect(findPipelineRef().exists()).toBe(true);
+ expect(findPipelineSha().exists()).toBe(true);
+ });
+
+ it('does not show any pipeline details when no information exists', () => {
+ mountComponent(null);
+
+ expect(findPipelineRef().exists()).toBe(false);
+ expect(findPipelineSha().exists()).toBe(false);
+ expect(findManualPublish().text()).toBe(PublishMethod.i18n.MANUALLY_PUBLISHED);
+ });
+ });
+});
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 9438a2d2d72..70fc096fa44 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -86,6 +86,12 @@ export const dependencyLinks = () => [
},
];
+export const packageProject = () => ({
+ fullPath: 'gitlab-org/gitlab-test',
+ webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-test',
+ __typename: 'Project',
+});
+
export const packageVersions = () => [
{
createdAt: '2021-08-10T09:33:54Z',
@@ -249,3 +255,31 @@ export const packageDestroyFileMutationError = () => ({
},
],
});
+
+export const packagesListQuery = (type = 'group') => ({
+ data: {
+ [type]: {
+ packages: {
+ count: 2,
+ nodes: [
+ {
+ ...packageData(),
+ project: packageProject(),
+ tags: { nodes: packageTags() },
+ pipelines: {
+ nodes: packagePipelines(),
+ },
+ },
+ {
+ ...packageData(),
+ project: packageProject(),
+ tags: { nodes: [] },
+ pipelines: { nodes: [] },
+ },
+ ],
+ __typename: 'PackageConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
new file mode 100644
index 00000000000..d3a970e86eb
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
@@ -0,0 +1,189 @@
+import { GlSprintf, GlLink, GlToggle } from '@gitlab/ui';
+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 component from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
+import {
+ DEPENDENCY_PROXY_HEADER,
+ DEPENDENCY_PROXY_SETTINGS_DESCRIPTION,
+ DEPENDENCY_PROXY_DOCS_PATH,
+} from '~/packages_and_registries/settings/group/constants';
+
+import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql';
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import { updateGroupDependencyProxySettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ dependencyProxySettings,
+ dependencyProxySettingMutationMock,
+ groupPackageSettingsMock,
+ dependencyProxySettingMutationErrorMock,
+} from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
+
+const localVue = createLocalVue();
+
+describe('DependencyProxySettings', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const defaultProvide = {
+ defaultExpanded: false,
+ groupPath: 'foo_group_path',
+ };
+
+ localVue.use(VueApollo);
+
+ const mountComponent = ({
+ provide = defaultProvide,
+ mutationResolver = jest.fn().mockResolvedValue(dependencyProxySettingMutationMock()),
+ isLoading = false,
+ } = {}) => {
+ const requestHandlers = [[updateDependencyProxySettings, mutationResolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(component, {
+ localVue,
+ apolloProvider,
+ provide,
+ propsData: {
+ dependencyProxySettings: dependencyProxySettings(),
+ isLoading,
+ },
+ stubs: {
+ GlSprintf,
+ SettingsBlock,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findDescription = () => wrapper.find('[data-testid="description"');
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findToggle = () => wrapper.findComponent(GlToggle);
+
+ const fillApolloCache = () => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getGroupPackagesSettingsQuery,
+ variables: {
+ fullPath: defaultProvide.groupPath,
+ },
+ ...groupPackageSettingsMock,
+ });
+ };
+
+ const emitSettingsUpdate = (value = false) => {
+ findToggle().vm.$emit('change', value);
+ };
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('passes the correct props to settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().props('defaultExpanded')).toBe(false);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(DEPENDENCY_PROXY_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(DEPENDENCY_PROXY_SETTINGS_DESCRIPTION);
+ });
+
+ it('has the correct link', () => {
+ mountComponent();
+
+ expect(findLink().attributes()).toMatchObject({
+ href: DEPENDENCY_PROXY_DOCS_PATH,
+ });
+ expect(findLink().text()).toBe('Learn more');
+ });
+
+ describe('settings update', () => {
+ describe('success state', () => {
+ it('emits a success event', async () => {
+ mountComponent();
+
+ fillApolloCache();
+ emitSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('success')).toEqual([[]]);
+ });
+
+ it('has an optimistic response', () => {
+ mountComponent();
+
+ fillApolloCache();
+
+ expect(findToggle().props('value')).toBe(true);
+
+ emitSettingsUpdate();
+
+ expect(updateGroupDependencyProxySettingsOptimisticResponse).toHaveBeenCalledWith({
+ enabled: false,
+ });
+ });
+ });
+
+ describe('errors', () => {
+ it('mutation payload with root level errors', async () => {
+ const mutationResolver = jest
+ .fn()
+ .mockResolvedValue(dependencyProxySettingMutationErrorMock);
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ emitSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[]]);
+ });
+
+ it.each`
+ type | mutationResolver
+ ${'local'} | ${jest.fn().mockResolvedValue(dependencyProxySettingMutationMock({ errors: ['foo'] }))}
+ ${'network'} | ${jest.fn().mockRejectedValue()}
+ `('mutation payload with $type error', async ({ mutationResolver }) => {
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+ emitSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[]]);
+ });
+ });
+ });
+
+ describe('when isLoading is true', () => {
+ it('disables enable toggle', () => {
+ mountComponent({ isLoading: true });
+
+ expect(findToggle().props('disabled')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js
index 0bbb1ce3436..79c2f811c08 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js
@@ -143,4 +143,18 @@ describe('Duplicates Settings', () => {
expect(findInputGroup().exists()).toBe(false);
});
});
+
+ describe('loading', () => {
+ beforeEach(() => {
+ mountComponent({ ...defaultProps, loading: true });
+ });
+
+ it('disables the enable toggle', () => {
+ expect(findToggle().props('disabled')).toBe(true);
+ });
+
+ it('disables the form input', () => {
+ expect(findInput().attributes('disabled')).toBe('true');
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index f2877a1f2a5..e4d62bc6a6e 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -1,28 +1,16 @@
-import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
-import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
+import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
+import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
+
import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
-import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
-import {
- PACKAGE_SETTINGS_HEADER,
- PACKAGE_SETTINGS_DESCRIPTION,
- PACKAGES_DOCS_PATH,
- ERROR_UPDATING_SETTINGS,
- SUCCESS_UPDATING_SETTINGS,
-} from '~/packages_and_registries/settings/group/constants';
-import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
-import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
-import {
- groupPackageSettingsMock,
- groupPackageSettingsMutationMock,
- groupPackageSettingsMutationErrorMock,
-} from '../mock_data';
+import { groupPackageSettingsMock, packageSettings, dependencyProxySettings } from '../mock_data';
jest.mock('~/flash');
@@ -36,20 +24,16 @@ describe('Group Settings App', () => {
const defaultProvide = {
defaultExpanded: false,
groupPath: 'foo_group_path',
+ dependencyProxyAvailable: true,
};
const mountComponent = ({
- provide = defaultProvide,
resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock),
- mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()),
- data = {},
+ provide = defaultProvide,
} = {}) => {
localVue.use(VueApollo);
- const requestHandlers = [
- [getGroupPackagesSettingsQuery, resolver],
- [updateNamespacePackageSettings, mutationResolver],
- ];
+ const requestHandlers = [[getGroupPackagesSettingsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
@@ -57,17 +41,6 @@ describe('Group Settings App', () => {
localVue,
apolloProvider,
provide,
- data() {
- return {
- ...data,
- };
- },
- stubs: {
- GlSprintf,
- SettingsBlock,
- MavenSettings,
- GenericSettings,
- },
mocks: {
$toast: {
show,
@@ -84,274 +57,89 @@ describe('Group Settings App', () => {
wrapper.destroy();
});
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
- const findDescription = () => wrapper.find('[data-testid="description"');
- const findLink = () => wrapper.findComponent(GlLink);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findMavenSettings = () => wrapper.findComponent(MavenSettings);
- const findMavenDuplicatedSettings = () => findMavenSettings().findComponent(DuplicatesSettings);
- const findGenericSettings = () => wrapper.findComponent(GenericSettings);
- const findGenericDuplicatedSettings = () =>
- findGenericSettings().findComponent(DuplicatesSettings);
+ const findPackageSettings = () => wrapper.findComponent(PackagesSettings);
+ const findDependencyProxySettings = () => wrapper.findComponent(DependencyProxySettings);
const waitForApolloQueryAndRender = async () => {
await waitForPromises();
- await wrapper.vm.$nextTick();
- };
-
- const emitSettingsUpdate = (override) => {
- findMavenDuplicatedSettings().vm.$emit('update', {
- mavenDuplicateExceptionRegex: ')',
- ...override,
- });
+ await nextTick();
};
- it('renders a settings block', () => {
- mountComponent();
-
- expect(findSettingsBlock().exists()).toBe(true);
- });
-
- it('passes the correct props to settings block', () => {
- mountComponent();
-
- expect(findSettingsBlock().props('defaultExpanded')).toBe(false);
- });
-
- it('has the correct header text', () => {
- mountComponent();
-
- expect(wrapper.text()).toContain(PACKAGE_SETTINGS_HEADER);
- });
-
- it('has the correct description text', () => {
- mountComponent();
-
- expect(findDescription().text()).toMatchInterpolatedText(PACKAGE_SETTINGS_DESCRIPTION);
- });
-
- it('has the correct link', () => {
- mountComponent();
-
- expect(findLink().attributes()).toMatchObject({
- href: PACKAGES_DOCS_PATH,
- target: '_blank',
- });
- expect(findLink().text()).toBe('Learn more.');
- });
-
- it('calls the graphql API with the proper variables', () => {
- const resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock);
- mountComponent({ resolver });
-
- expect(resolver).toHaveBeenCalledWith({
- fullPath: defaultProvide.groupPath,
- });
- });
-
- describe('maven settings', () => {
- it('exists', () => {
+ describe.each`
+ finder | entityProp | entityValue | successMessage | errorMessage
+ ${findPackageSettings} | ${'packageSettings'} | ${packageSettings()} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
+ ${findDependencyProxySettings} | ${'dependencyProxySettings'} | ${dependencyProxySettings()} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'}
+ `('settings blocks', ({ finder, entityProp, entityValue, successMessage, errorMessage }) => {
+ beforeEach(() => {
mountComponent();
-
- expect(findMavenSettings().exists()).toBe(true);
+ return waitForApolloQueryAndRender();
});
- it('assigns duplication allowness and exception props', async () => {
- mountComponent();
-
- expect(findMavenDuplicatedSettings().props('loading')).toBe(true);
-
- await waitForApolloQueryAndRender();
-
- const {
- mavenDuplicatesAllowed,
- mavenDuplicateExceptionRegex,
- } = groupPackageSettingsMock.data.group.packageSettings;
-
- expect(findMavenDuplicatedSettings().props()).toMatchObject({
- duplicatesAllowed: mavenDuplicatesAllowed,
- duplicateExceptionRegex: mavenDuplicateExceptionRegex,
- duplicateExceptionRegexError: '',
- loading: false,
- });
+ it('renders the settings block', () => {
+ expect(finder().exists()).toBe(true);
});
- it('on update event calls the mutation', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
- mountComponent({ mutationResolver });
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- expect(mutationResolver).toHaveBeenCalledWith({
- input: { mavenDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ it('binds the correctProps', () => {
+ expect(finder().props()).toMatchObject({
+ isLoading: false,
+ [entityProp]: entityValue,
});
});
- });
-
- describe('generic settings', () => {
- it('exists', () => {
- mountComponent();
-
- expect(findGenericSettings().exists()).toBe(true);
- });
-
- it('assigns duplication allowness and exception props', async () => {
- mountComponent();
-
- expect(findGenericDuplicatedSettings().props('loading')).toBe(true);
-
- await waitForApolloQueryAndRender();
-
- const {
- genericDuplicatesAllowed,
- genericDuplicateExceptionRegex,
- } = groupPackageSettingsMock.data.group.packageSettings;
- expect(findGenericDuplicatedSettings().props()).toMatchObject({
- duplicatesAllowed: genericDuplicatesAllowed,
- duplicateExceptionRegex: genericDuplicateExceptionRegex,
- duplicateExceptionRegexError: '',
- loading: false,
+ describe('success event', () => {
+ it('shows a success toast', () => {
+ finder().vm.$emit('success');
+ expect(show).toHaveBeenCalledWith(successMessage);
});
- });
- it('on update event calls the mutation', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
- mountComponent({ mutationResolver });
+ it('hides the error alert', async () => {
+ finder().vm.$emit('error');
+ await nextTick();
- await waitForApolloQueryAndRender();
+ expect(findAlert().exists()).toBe(true);
- findMavenDuplicatedSettings().vm.$emit('update', {
- genericDuplicateExceptionRegex: ')',
- });
+ finder().vm.$emit('success');
+ await nextTick();
- expect(mutationResolver).toHaveBeenCalledWith({
- input: { genericDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ expect(findAlert().exists()).toBe(false);
});
});
- });
-
- describe('settings update', () => {
- describe('success state', () => {
- it('shows a success alert', async () => {
- mountComponent();
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- await waitForPromises();
-
- expect(show).toHaveBeenCalledWith(SUCCESS_UPDATING_SETTINGS);
- });
-
- it('has an optimistic response', async () => {
- const mavenDuplicateExceptionRegex = 'latest[main]something';
- mountComponent();
- await waitForApolloQueryAndRender();
-
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe('');
-
- emitSettingsUpdate({ mavenDuplicateExceptionRegex });
-
- // wait for apollo to update the model with the optimistic response
- await wrapper.vm.$nextTick();
-
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe(
- mavenDuplicateExceptionRegex,
- );
-
- // wait for the call to resolve
- await waitForPromises();
-
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe(
- mavenDuplicateExceptionRegex,
- );
+ describe('error event', () => {
+ beforeEach(() => {
+ finder().vm.$emit('error');
+ return nextTick();
});
- });
- describe('errors', () => {
- const verifyAlert = () => {
+ it('shows an alert', () => {
expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(ERROR_UPDATING_SETTINGS);
- expect(findAlert().props('variant')).toBe('warning');
- };
-
- it('mutation payload with root level errors', async () => {
- // note this is a complex test that covers all the path around errors that are shown in the form
- // it's one single it case, due to the expensive preparation and execution
- const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationErrorMock);
- mountComponent({ mutationResolver });
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- await waitForApolloQueryAndRender();
-
- // errors are bound to the component
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe(
- groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message,
- );
-
- // general error message is shown
-
- verifyAlert();
-
- emitSettingsUpdate();
-
- await wrapper.vm.$nextTick();
-
- // errors are reset on mutation call
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe('');
});
- it.each`
- type | mutationResolver
- ${'local'} | ${jest.fn().mockResolvedValue(groupPackageSettingsMutationMock({ errors: ['foo'] }))}
- ${'network'} | ${jest.fn().mockRejectedValue()}
- `('mutation payload with $type error', async ({ mutationResolver }) => {
- mountComponent({ mutationResolver });
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- await waitForPromises();
-
- verifyAlert();
+ it('alert has the right text', () => {
+ expect(findAlert().text()).toBe(errorMessage);
});
- it('a successful request dismisses the alert', async () => {
- mountComponent({ data: { alertMessage: 'foo' } });
-
- await waitForApolloQueryAndRender();
-
+ it('dismissing the alert removes it', async () => {
expect(findAlert().exists()).toBe(true);
- emitSettingsUpdate();
+ findAlert().vm.$emit('dismiss');
- await waitForPromises();
+ await nextTick();
expect(findAlert().exists()).toBe(false);
});
+ });
+ });
- it('dismiss event from alert dismiss it from the page', async () => {
- mountComponent({ data: { alertMessage: 'foo' } });
-
- await waitForApolloQueryAndRender();
-
- expect(findAlert().exists()).toBe(true);
-
- findAlert().vm.$emit('dismiss');
-
- await wrapper.vm.$nextTick();
+ describe('when the dependency proxy is not available', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { ...defaultProvide, dependencyProxyAvailable: false } });
+ return waitForApolloQueryAndRender();
+ });
- expect(findAlert().exists()).toBe(false);
- });
+ it('the setting block is hidden', () => {
+ expect(findDependencyProxySettings().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
new file mode 100644
index 00000000000..693af21e24a
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -0,0 +1,277 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
+import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
+import component from '~/packages_and_registries/settings/group/components/packages_settings.vue';
+import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+import {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ PACKAGES_DOCS_PATH,
+} from '~/packages_and_registries/settings/group/constants';
+
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ packageSettings,
+ groupPackageSettingsMock,
+ groupPackageSettingsMutationMock,
+ groupPackageSettingsMutationErrorMock,
+} from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
+
+const localVue = createLocalVue();
+
+describe('Packages Settings', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const defaultProvide = {
+ defaultExpanded: false,
+ groupPath: 'foo_group_path',
+ };
+
+ const mountComponent = ({
+ mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()),
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[updateNamespacePackageSettings, mutationResolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(component, {
+ localVue,
+ apolloProvider,
+ provide: defaultProvide,
+ propsData: {
+ packageSettings: packageSettings(),
+ },
+ stubs: {
+ GlSprintf,
+ SettingsBlock,
+ MavenSettings,
+ GenericSettings,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findDescription = () => wrapper.findByTestId('description');
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findMavenSettings = () => wrapper.findComponent(MavenSettings);
+ const findMavenDuplicatedSettings = () => findMavenSettings().findComponent(DuplicatesSettings);
+ const findGenericSettings = () => wrapper.findComponent(GenericSettings);
+ const findGenericDuplicatedSettings = () =>
+ findGenericSettings().findComponent(DuplicatesSettings);
+
+ const fillApolloCache = () => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getGroupPackagesSettingsQuery,
+ variables: {
+ fullPath: defaultProvide.groupPath,
+ },
+ ...groupPackageSettingsMock,
+ });
+ };
+
+ const emitMavenSettingsUpdate = (override) => {
+ findMavenDuplicatedSettings().vm.$emit('update', {
+ mavenDuplicateExceptionRegex: ')',
+ ...override,
+ });
+ };
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('passes the correct props to settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().props('defaultExpanded')).toBe(false);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(PACKAGE_SETTINGS_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(PACKAGE_SETTINGS_DESCRIPTION);
+ });
+
+ it('has the correct link', () => {
+ mountComponent();
+
+ expect(findLink().attributes()).toMatchObject({
+ href: PACKAGES_DOCS_PATH,
+ target: '_blank',
+ });
+ expect(findLink().text()).toBe('Learn more.');
+ });
+
+ describe('maven settings', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findMavenSettings().exists()).toBe(true);
+ });
+
+ it('assigns duplication allowness and exception props', async () => {
+ mountComponent();
+
+ const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings();
+
+ expect(findMavenDuplicatedSettings().props()).toMatchObject({
+ duplicatesAllowed: mavenDuplicatesAllowed,
+ duplicateExceptionRegex: mavenDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
+ loading: false,
+ });
+ });
+
+ it('on update event calls the mutation', () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ emitMavenSettingsUpdate();
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { mavenDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
+ describe('generic settings', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findGenericSettings().exists()).toBe(true);
+ });
+
+ it('assigns duplication allowness and exception props', async () => {
+ mountComponent();
+
+ const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings();
+
+ expect(findGenericDuplicatedSettings().props()).toMatchObject({
+ duplicatesAllowed: genericDuplicatesAllowed,
+ duplicateExceptionRegex: genericDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
+ loading: false,
+ });
+ });
+
+ it('on update event calls the mutation', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ findMavenDuplicatedSettings().vm.$emit('update', {
+ genericDuplicateExceptionRegex: ')',
+ });
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { genericDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
+ describe('settings update', () => {
+ describe('success state', () => {
+ it('emits a success event', async () => {
+ mountComponent();
+
+ fillApolloCache();
+ emitMavenSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('success')).toEqual([[]]);
+ });
+
+ it('has an optimistic response', () => {
+ const mavenDuplicateExceptionRegex = 'latest[main]something';
+ mountComponent();
+
+ fillApolloCache();
+
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe('');
+
+ emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex });
+
+ expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
+ ...packageSettings(),
+ mavenDuplicateExceptionRegex,
+ });
+ });
+ });
+
+ describe('errors', () => {
+ it('mutation payload with root level errors', async () => {
+ // note this is a complex test that covers all the path around errors that are shown in the form
+ // it's one single it case, due to the expensive preparation and execution
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationErrorMock);
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ emitMavenSettingsUpdate();
+
+ await waitForPromises();
+
+ // errors are bound to the component
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe(
+ groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message,
+ );
+
+ // general error message is shown
+
+ expect(wrapper.emitted('error')).toEqual([[]]);
+
+ emitMavenSettingsUpdate();
+
+ await wrapper.vm.$nextTick();
+
+ // errors are reset on mutation call
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe('');
+ });
+
+ it.each`
+ type | mutationResolver
+ ${'local'} | ${jest.fn().mockResolvedValue(groupPackageSettingsMutationMock({ errors: ['foo'] }))}
+ ${'network'} | ${jest.fn().mockRejectedValue()}
+ `('mutation payload with $type error', async ({ mutationResolver }) => {
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+ emitMavenSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
index 03133bf1158..9d8504a1124 100644
--- a/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
@@ -4,14 +4,16 @@ import { updateGroupPackageSettings } from '~/packages_and_registries/settings/g
describe('Package and Registries settings group cache updates', () => {
let client;
- const payload = {
- data: {
- updateNamespacePackageSettings: {
- packageSettings: {
- mavenDuplicatesAllowed: false,
- mavenDuplicateExceptionRegex: 'latest[main]something',
- },
- },
+ const updateNamespacePackageSettingsPayload = {
+ packageSettings: {
+ mavenDuplicatesAllowed: false,
+ mavenDuplicateExceptionRegex: 'latest[main]something',
+ },
+ };
+
+ const updateDependencyProxySettingsPayload = {
+ dependencyProxySetting: {
+ enabled: false,
},
};
@@ -21,6 +23,9 @@ describe('Package and Registries settings group cache updates', () => {
mavenDuplicatesAllowed: true,
mavenDuplicateExceptionRegex: '',
},
+ dependencyProxySetting: {
+ enabled: true,
+ },
},
};
@@ -35,22 +40,35 @@ describe('Package and Registries settings group cache updates', () => {
writeQuery: jest.fn(),
};
});
- describe('updateGroupPackageSettings', () => {
- it('calls readQuery', () => {
- updateGroupPackageSettings('foo')(client, payload);
- expect(client.readQuery).toHaveBeenCalledWith(queryAndVariables);
- });
-
- it('writes the correct result in the cache', () => {
- updateGroupPackageSettings('foo')(client, payload);
- expect(client.writeQuery).toHaveBeenCalledWith({
- ...queryAndVariables,
- data: {
- group: {
- ...payload.data.updateNamespacePackageSettings,
+
+ describe.each`
+ updateNamespacePackageSettings | updateDependencyProxySettings
+ ${updateNamespacePackageSettingsPayload} | ${updateDependencyProxySettingsPayload}
+ ${undefined} | ${updateDependencyProxySettingsPayload}
+ ${updateNamespacePackageSettingsPayload} | ${undefined}
+ ${undefined} | ${undefined}
+ `(
+ 'updateGroupPackageSettings',
+ ({ updateNamespacePackageSettings, updateDependencyProxySettings }) => {
+ const payload = { data: { updateNamespacePackageSettings, updateDependencyProxySettings } };
+ it('calls readQuery', () => {
+ updateGroupPackageSettings('foo')(client, payload);
+ expect(client.readQuery).toHaveBeenCalledWith(queryAndVariables);
+ });
+
+ it('writes the correct result in the cache', () => {
+ updateGroupPackageSettings('foo')(client, payload);
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ ...queryAndVariables,
+ data: {
+ group: {
+ ...cacheMock.group,
+ ...payload.data.updateNamespacePackageSettings,
+ ...payload.data.updateDependencyProxySettings,
+ },
},
- },
+ });
});
- });
- });
+ },
+ );
});
diff --git a/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js b/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js
index a3c53d5768a..debeb9aa89c 100644
--- a/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js
@@ -1,4 +1,7 @@
-import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ updateGroupPackagesSettingsOptimisticResponse,
+ updateGroupDependencyProxySettingsOptimisticResponse,
+} from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
describe('Optimistic responses', () => {
describe('updateGroupPackagesSettingsOptimisticResponse', () => {
@@ -17,4 +20,22 @@ describe('Optimistic responses', () => {
`);
});
});
+
+ describe('updateGroupDependencyProxySettingsOptimisticResponse', () => {
+ it('returns the correct structure', () => {
+ expect(updateGroupDependencyProxySettingsOptimisticResponse({ foo: 'bar' }))
+ .toMatchInlineSnapshot(`
+ Object {
+ "__typename": "Mutation",
+ "updateDependencyProxySettings": Object {
+ "__typename": "UpdateDependencyProxySettingsPayload",
+ "dependencyProxySetting": Object {
+ "foo": "bar",
+ },
+ "errors": Array [],
+ },
+ }
+ `);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index 65119e288a1..81ba0795b7d 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -1,12 +1,20 @@
+export const packageSettings = () => ({
+ mavenDuplicatesAllowed: true,
+ mavenDuplicateExceptionRegex: '',
+ genericDuplicatesAllowed: true,
+ genericDuplicateExceptionRegex: '',
+});
+
+export const dependencyProxySettings = () => ({
+ enabled: true,
+});
+
export const groupPackageSettingsMock = {
data: {
group: {
- packageSettings: {
- mavenDuplicatesAllowed: true,
- mavenDuplicateExceptionRegex: '',
- genericDuplicatesAllowed: true,
- genericDuplicateExceptionRegex: '',
- },
+ fullPath: 'foo_group_path',
+ packageSettings: packageSettings(),
+ dependencyProxySetting: dependencyProxySettings(),
},
},
};
@@ -26,6 +34,16 @@ export const groupPackageSettingsMutationMock = (override) => ({
},
});
+export const dependencyProxySettingMutationMock = (override) => ({
+ data: {
+ updateDependencyProxySettings: {
+ dependencyProxySetting: dependencyProxySettings(),
+ errors: [],
+ ...override,
+ },
+ },
+});
+
export const groupPackageSettingsMutationErrorMock = {
errors: [
{
@@ -50,3 +68,23 @@ export const groupPackageSettingsMutationErrorMock = {
},
],
};
+export const dependencyProxySettingMutationErrorMock = {
+ errors: [
+ {
+ message: 'Some error',
+ locations: [{ line: 1, column: 41 }],
+ extensions: {
+ value: {
+ enabled: 'gitlab-org',
+ },
+ problems: [
+ {
+ path: ['enabled'],
+ explanation: 'explaination',
+ message: 'message',
+ },
+ ],
+ },
+ },
+ ],
+};
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
index cf554717127..2719e917a9b 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
@@ -100,6 +100,12 @@ Array [
"variable": 30,
},
Object {
+ "default": false,
+ "key": "SIXTY_DAYS",
+ "label": "60 days",
+ "variable": 60,
+ },
+ Object {
"default": true,
"key": "NINETY_DAYS",
"label": "90 days",
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
index 1009db46401..9938357ed24 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -44,7 +44,7 @@ exports[`Settings Form Keep Regex matches snapshot 1`] = `
exports[`Settings Form OlderThan matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="older-than-dropdown"
- formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
+ formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Remove tags older than:"
name="older-than"
value="FOURTEEN_DAYS"
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
new file mode 100644
index 00000000000..c579aa2f2da
--- /dev/null
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -0,0 +1,93 @@
+import { mount } from '@vue/test-utils';
+import Api from '~/api';
+import NamespaceSelect from '~/pages/admin/projects/components/namespace_select.vue';
+
+describe('Dropdown select component', () => {
+ let wrapper;
+
+ const mountDropdown = (propsData) => {
+ wrapper = mount(NamespaceSelect, { propsData });
+ };
+
+ const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
+ const findNamespaceInput = () => wrapper.find('[data-testid="hidden-input"]');
+ const findFilterInput = () => wrapper.find('.namespace-search-box input');
+ const findDropdownOption = (match) => {
+ const buttons = wrapper
+ .findAll('button.dropdown-item')
+ .filter((node) => node.text().match(match));
+ return buttons.length ? buttons.at(0) : buttons;
+ };
+
+ const setFieldValue = async (field, value) => {
+ await field.setValue(value);
+ field.trigger('blur');
+ };
+
+ beforeEach(() => {
+ setFixtures('<div class="test-container"></div>');
+
+ jest.spyOn(Api, 'namespaces').mockImplementation((_, callback) =>
+ callback([
+ { id: 10, kind: 'user', full_path: 'Administrator' },
+ { id: 20, kind: 'group', full_path: 'GitLab Org' },
+ ]),
+ );
+ });
+
+ it('creates a hidden input if fieldName is provided', () => {
+ mountDropdown({ fieldName: 'namespace-input' });
+
+ expect(findNamespaceInput()).toExist();
+ expect(findNamespaceInput().attributes('name')).toBe('namespace-input');
+ });
+
+ describe('clicking dropdown options', () => {
+ it('retrieves namespaces based on filter query', async () => {
+ mountDropdown();
+
+ await setFieldValue(findFilterInput(), 'test');
+
+ expect(Api.namespaces).toHaveBeenCalledWith('test', expect.anything());
+ });
+
+ it('updates the dropdown value based upon selection', async () => {
+ mountDropdown({ fieldName: 'namespace-input' });
+
+ // 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();
+
+ findDropdownOption('user: Administrator').trigger('click');
+ await wrapper.vm.$nextTick();
+
+ expect(findNamespaceInput().attributes('value')).toBe('10');
+ expect(findDropdownToggle().text()).toBe('user: Administrator');
+ });
+
+ it('triggers a setNamespace event upon selection', async () => {
+ mountDropdown();
+
+ // wait for dropdown options to populate
+ await wrapper.vm.$nextTick();
+
+ findDropdownOption('group: GitLab Org').trigger('click');
+
+ expect(wrapper.emitted('setNamespace')).toHaveLength(1);
+ expect(wrapper.emitted('setNamespace')[0][0]).toBe(20);
+ });
+
+ it('displays "Any Namespace" option when showAny prop provided', () => {
+ mountDropdown({ showAny: true });
+ expect(wrapper.text()).toContain('Any namespace');
+ });
+
+ it('does not display "Any Namespace" option when showAny prop not provided', () => {
+ mountDropdown();
+ expect(wrapper.text()).not.toContain('Any namespace');
+ });
+ });
+});
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
new file mode 100644
index 00000000000..d6b394a42c6
--- /dev/null
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -0,0 +1,175 @@
+import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import PaginationBar from '~/import_entities/components/pagination_bar.vue';
+import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+describe('BulkImportsHistoryApp', () => {
+ const API_URL = '/api/v4/bulk_imports/entities';
+
+ const DEFAULT_HEADERS = {
+ 'x-page': 1,
+ 'x-per-page': 20,
+ 'x-next-page': 2,
+ 'x-total': 22,
+ 'x-total-pages': 2,
+ 'x-prev-page': null,
+ };
+ const DUMMY_RESPONSE = [
+ {
+ id: 1,
+ bulk_import_id: 1,
+ status: 'finished',
+ source_full_path: 'top-level-group-12',
+ destination_name: 'top-level-group-12',
+ destination_namespace: 'h5bp',
+ created_at: '2021-07-08T10:03:44.743Z',
+ failures: [],
+ },
+ {
+ id: 2,
+ bulk_import_id: 2,
+ status: 'failed',
+ source_full_path: 'autodevops-demo',
+ destination_name: 'autodevops-demo',
+ destination_namespace: 'flightjs',
+ parent_id: null,
+ namespace_id: null,
+ project_id: null,
+ created_at: '2021-07-13T12:52:26.664Z',
+ updated_at: '2021-07-13T13:34:49.403Z',
+ failures: [
+ {
+ pipeline_class: 'BulkImports::Groups::Pipelines::GroupPipeline',
+ pipeline_step: 'loader',
+ exception_class: 'ActiveRecord::RecordNotUnique',
+ correlation_id_value: '01FAFYSYZ7XPF3P9NSMTS693SZ',
+ created_at: '2021-07-13T13:34:49.344Z',
+ },
+ ],
+ },
+ ];
+
+ let wrapper;
+ let mock;
+
+ function createComponent({ shallow = true } = {}) {
+ const mountFn = shallow ? shallowMount : mount;
+ wrapper = mountFn(BulkImportsHistoryApp);
+ }
+
+ const originalApiVersion = gon.api_version;
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ afterAll(() => {
+ gon.api_version = originalApiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('general behavior', () => {
+ it('renders loading state when loading', () => {
+ createComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders empty state when no data is available', async () => {
+ mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ });
+
+ it('renders table with data when history is available', async () => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+
+ const table = wrapper.find(GlTable);
+ expect(table.exists()).toBe(true);
+ // can't use .props() or .attributes() here
+ expect(table.vm.$attrs.items).toHaveLength(DUMMY_RESPONSE.length);
+ });
+
+ it('changes page when requested by pagination bar', async () => {
+ const NEW_PAGE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE }));
+ });
+ });
+
+ it('changes page size when requested by pagination bar', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE }),
+ );
+ });
+
+ describe('details button', () => {
+ beforeEach(() => {
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent({ shallow: false });
+ return axios.waitForAll();
+ });
+
+ it('renders details button if relevant item has failures', async () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
+ ).toBe(true);
+ });
+
+ it('does not render details button if relevant item has no failures', () => {
+ expect(
+ extendedWrapper(wrapper.find('tbody').findAll('tr').at(0)).findByText('Details').exists(),
+ ).toBe(false);
+ });
+
+ it('expands details when details button is clicked', async () => {
+ const ORIGINAL_ROW_INDEX = 1;
+ await extendedWrapper(wrapper.find('tbody').findAll('tr').at(ORIGINAL_ROW_INDEX))
+ .findByText('Details')
+ .trigger('click');
+
+ const detailsRowContent = wrapper
+ .find('tbody')
+ .findAll('tr')
+ .at(ORIGINAL_ROW_INDEX + 1)
+ .find('pre');
+
+ expect(detailsRowContent.exists()).toBe(true);
+ expect(JSON.parse(detailsRowContent.text())).toStrictEqual(DUMMY_RESPONSE[1].failures);
+ });
+ });
+});
diff --git a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
new file mode 100644
index 00000000000..b722ac1e97b
--- /dev/null
+++ b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
@@ -0,0 +1,92 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
+ I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
+} from '~/pages/profiles/password_prompt/constants';
+import PasswordPromptModal from '~/pages/profiles/password_prompt/password_prompt_modal.vue';
+
+const createComponent = ({ props }) => {
+ return shallowMountExtended(PasswordPromptModal, {
+ propsData: {
+ ...props,
+ },
+ });
+};
+
+describe('Password prompt modal', () => {
+ let wrapper;
+
+ const mockPassword = 'not+fake+shady+password';
+ const mockEvent = { preventDefault: jest.fn() };
+ const handleConfirmPasswordSpy = jest.fn();
+
+ const findField = () => wrapper.findByTestId('password-prompt-field');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findConfirmBtn = () => findModal().props('actionPrimary');
+ const findConfirmBtnDisabledState = () =>
+ findModal().props('actionPrimary').attributes[2].disabled;
+
+ const findCancelBtn = () => findModal().props('actionCancel');
+
+ const submitModal = () => findModal().vm.$emit('primary', mockEvent);
+ const setPassword = (newPw) => findField().vm.$emit('input', newPw);
+
+ beforeEach(() => {
+ wrapper = createComponent({
+ props: {
+ handleConfirmPassword: handleConfirmPasswordSpy,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the password field', () => {
+ expect(findField().exists()).toBe(true);
+ });
+
+ it('renders the confirm button', () => {
+ expect(findConfirmBtn().text).toEqual(I18N_PASSWORD_PROMPT_CONFIRM_BUTTON);
+ });
+
+ it('renders the cancel button', () => {
+ expect(findCancelBtn().text).toEqual(I18N_PASSWORD_PROMPT_CANCEL_BUTTON);
+ });
+
+ describe('confirm button', () => {
+ describe('with a valid password', () => {
+ it('calls the `handleConfirmPassword` method when clicked', async () => {
+ setPassword(mockPassword);
+ submitModal();
+
+ await wrapper.vm.$nextTick();
+
+ expect(handleConfirmPasswordSpy).toHaveBeenCalledTimes(1);
+ expect(handleConfirmPasswordSpy).toHaveBeenCalledWith(mockPassword);
+ });
+
+ it('enables the confirm button', async () => {
+ setPassword(mockPassword);
+
+ expect(findConfirmBtnDisabledState()).toBe(true);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findConfirmBtnDisabledState()).toBe(false);
+ });
+ });
+
+ it('without a valid password is disabled', async () => {
+ setPassword('');
+
+ expect(findConfirmBtnDisabledState()).toBe(true);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findConfirmBtnDisabledState()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index 417567c9f4c..43361bb6f24 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -12,11 +12,11 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<gl-dropdown-stub
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
- showhighlighteditemstitle="true"
size="medium"
text="rspec"
variant="default"
diff --git a/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js b/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js
deleted file mode 100644
index 8a7f9229503..00000000000
--- a/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import { GlButton, GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import NewProjectUrlSelect from '~/pages/projects/new/components/new_project_url_select.vue';
-import searchQuery from '~/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
-
-describe('NewProjectUrlSelect component', () => {
- let wrapper;
-
- const data = {
- currentUser: {
- groups: {
- nodes: [
- {
- id: 'gid://gitlab/Group/26',
- fullPath: 'flightjs',
- },
- {
- id: 'gid://gitlab/Group/28',
- fullPath: 'h5bp',
- },
- ],
- },
- namespace: {
- id: 'gid://gitlab/Namespace/1',
- fullPath: 'root',
- },
- },
- };
-
- const localVue = createLocalVue();
- localVue.use(VueApollo);
-
- const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data })]];
- const apolloProvider = createMockApollo(requestHandlers);
-
- const provide = {
- namespaceFullPath: 'h5bp',
- namespaceId: '28',
- rootUrl: 'https://gitlab.com/',
- trackLabel: 'blank_project',
- };
-
- const mountComponent = ({ mountFn = shallowMount } = {}) =>
- mountFn(NewProjectUrlSelect, { localVue, apolloProvider, provide });
-
- const findButtonLabel = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findHiddenInput = () => wrapper.find('input');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the root url as a label', () => {
- wrapper = mountComponent();
-
- expect(findButtonLabel().text()).toBe(provide.rootUrl);
- expect(findButtonLabel().props('label')).toBe(true);
- });
-
- it('renders a dropdown with the initial namespace full path as the text', () => {
- wrapper = mountComponent();
-
- expect(findDropdown().props('text')).toBe(provide.namespaceFullPath);
- });
-
- it('renders a dropdown with the initial namespace id in the hidden input', () => {
- wrapper = mountComponent();
-
- expect(findHiddenInput().attributes('value')).toBe(provide.namespaceId);
- });
-
- it('renders expected dropdown items', async () => {
- wrapper = mountComponent({ mountFn: mount });
-
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
-
- const listItems = wrapper.findAll('li');
-
- expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
- expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[0].fullPath);
- expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
- expect(listItems.at(3).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
- expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath);
- });
-
- it('updates hidden input with selected namespace', async () => {
- wrapper = mountComponent();
-
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
-
- wrapper.findComponent(GlDropdownItem).vm.$emit('click');
-
- await wrapper.vm.$nextTick();
-
- expect(findHiddenInput().attributes()).toMatchObject({
- name: 'project[namespace_id]',
- value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
- });
- });
-
- it('tracks clicking on the dropdown', () => {
- wrapper = mountComponent();
-
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- findDropdown().vm.$emit('show');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', {
- label: provide.trackLabel,
- property: 'project_path',
- });
-
- unmockTracking();
- });
-});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index 2a3b07f95f2..53c1733eab9 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -10,7 +10,17 @@ describe('Timezone Dropdown', () => {
let $dropdownEl = null;
let $wrapper = null;
const tzListSel = '.dropdown-content ul li a.is-active';
- const tzDropdownToggleText = '.dropdown-toggle-text';
+
+ const initTimezoneDropdown = (options = {}) => {
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ ...options,
+ });
+ };
+
+ const findDropdownToggleText = () => $wrapper.find('.dropdown-toggle-text');
describe('Initialize', () => {
describe('with dropdown already loaded', () => {
@@ -18,16 +28,13 @@ describe('Timezone Dropdown', () => {
loadFixtures('pipeline_schedules/edit.html');
$wrapper = $('.dropdown');
$inputEl = $('#schedule_cron_timezone');
+ $inputEl.val('');
$dropdownEl = $('.js-timezone-dropdown');
-
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- });
});
it('can take an $inputEl in the constructor', () => {
+ initTimezoneDropdown();
+
const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
const tzValue = 'Asia/Colombo';
@@ -42,6 +49,8 @@ describe('Timezone Dropdown', () => {
});
it('will format data array of timezones into a list of offsets', () => {
+ initTimezoneDropdown();
+
const data = $dropdownEl.data('data');
const formatted = $wrapper.find(tzListSel).text();
@@ -50,10 +59,28 @@ describe('Timezone Dropdown', () => {
});
});
- it('will default the timezone to UTC', () => {
- const tz = $inputEl.val();
+ describe('when `allowEmpty` property is `false`', () => {
+ beforeEach(() => {
+ initTimezoneDropdown();
+ });
+
+ it('will default the timezone to UTC', () => {
+ const tz = $inputEl.val();
- expect(tz).toBe('UTC');
+ expect(tz).toBe('UTC');
+ });
+ });
+
+ describe('when `allowEmpty` property is `true`', () => {
+ beforeEach(() => {
+ initTimezoneDropdown({
+ allowEmpty: true,
+ });
+ });
+
+ it('will default the value of the input to an empty string', () => {
+ expect($inputEl.val()).toBe('');
+ });
});
});
@@ -68,23 +95,15 @@ describe('Timezone Dropdown', () => {
it('will populate the list of UTC offsets after the dropdown is loaded', () => {
expect($wrapper.find(tzListSel).length).toEqual(0);
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- });
+ initTimezoneDropdown();
expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length);
});
it('will call a provided handler when a new timezone is selected', () => {
const onSelectTimezone = jest.fn();
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- onSelectTimezone,
- });
+
+ initTimezoneDropdown({ onSelectTimezone });
$wrapper.find(tzListSel).first().trigger('click');
@@ -94,24 +113,15 @@ describe('Timezone Dropdown', () => {
it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => {
$inputEl.val('America/St_Johns');
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- displayFormat: (selectedItem) => formatTimezone(selectedItem),
- });
+ initTimezoneDropdown({ displayFormat: (selectedItem) => formatTimezone(selectedItem) });
- expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland');
+ expect(findDropdownToggleText().html()).toEqual('[UTC - 2.5] Newfoundland');
});
it('will call a provided `displayFormat` handler to format the dropdown value', () => {
const displayFormat = jest.fn();
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- displayFormat,
- });
+
+ initTimezoneDropdown({ displayFormat });
$wrapper.find(tzListSel).first().trigger('click');
diff --git a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
index e39a3904613..a29db961452 100644
--- a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
@@ -44,7 +44,7 @@ describe('preserve_url_fragment', () => {
});
it('when "remember-me" is present', () => {
- $('.omniauth-btn')
+ $('.js-oauth-login')
.parent('form')
.attr('action', (i, href) => `${href}?remember_me=1`);
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
index 85222f2ecbb..a43da4b0f19 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
@@ -112,11 +112,6 @@ describe('Pipeline Editor | Text editor component', () => {
it('configures editor with syntax highlight', () => {
expect(mockUse).toHaveBeenCalledTimes(1);
expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledWith({
- projectNamespace: mockProjectNamespace,
- projectPath: mockProjectPath,
- ref: mockCommitSha,
- });
});
});
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 753682d438b..44656b2b67d 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
@@ -5,22 +5,18 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
-const mockProvide = {
- projectFullPath: mockProjectFullPath,
-};
-
describe('Pipeline Status', () => {
let wrapper;
let mockApollo;
let mockPipelineQuery;
- const createComponentWithApollo = () => {
+ const createComponentWithApollo = (glFeatures = {}) => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
@@ -30,19 +26,23 @@ describe('Pipeline Status', () => {
propsData: {
commitSha: mockCommitSha,
},
- provide: mockProvide,
+ provide: {
+ glFeatures,
+ projectFullPath: mockProjectFullPath,
+ },
stubs: { GlLink, GlSprintf },
});
};
const findIcon = () => wrapper.findComponent(GlIcon);
- const findCiIcon = () => wrapper.findComponent(CiIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPipelineEditorMiniGraph = () => wrapper.findComponent(PipelineEditorMiniGraph);
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 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"]');
beforeEach(() => {
mockPipelineQuery = jest.fn();
@@ -50,9 +50,7 @@ describe('Pipeline Status', () => {
afterEach(() => {
mockPipelineQuery.mockReset();
-
wrapper.destroy();
- wrapper = null;
});
describe('loading icon', () => {
@@ -73,13 +71,13 @@ describe('Pipeline Status', () => {
describe('when querying data', () => {
describe('when data is set', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockPipelineQuery.mockResolvedValue({
- data: { project: mockProjectPipeline },
+ data: { project: mockProjectPipeline() },
});
createComponentWithApollo();
- await waitForPromises();
+ waitForPromises();
});
it('query is called with correct variables', async () => {
@@ -91,20 +89,24 @@ describe('Pipeline Status', () => {
});
it('does not render error', () => {
- expect(findIcon().exists()).toBe(false);
+ expect(findPipelineErrorMsg().exists()).toBe(false);
});
it('renders pipeline data', () => {
const {
id,
detailedStatus: { detailsPath },
- } = mockProjectPipeline.pipeline;
+ } = mockProjectPipeline().pipeline;
- expect(findCiIcon().exists()).toBe(true);
+ expect(findStatusIcon().exists()).toBe(true);
expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`);
expect(findPipelineCommit().text()).toBe(mockCommitSha);
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
});
+
+ it('does not render the pipeline mini graph', () => {
+ expect(findPipelineEditorMiniGraph().exists()).toBe(false);
+ });
});
describe('when data cannot be fetched', () => {
@@ -121,11 +123,26 @@ describe('Pipeline Status', () => {
});
it('does not render pipeline data', () => {
- expect(findCiIcon().exists()).toBe(false);
+ expect(findStatusIcon().exists()).toBe(false);
expect(findPipelineId().exists()).toBe(false);
expect(findPipelineCommit().exists()).toBe(false);
expect(findPipelineViewBtn().exists()).toBe(false);
});
});
});
+
+ describe('when feature flag for pipeline mini graph is enabled', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue({
+ data: { project: mockProjectPipeline() },
+ });
+
+ createComponentWithApollo({ pipelineEditorMiniGraph: true });
+ waitForPromises();
+ });
+
+ it('renders the pipeline mini graph', () => {
+ expect(findPipelineEditorMiniGraph().exists()).toBe(true);
+ });
+ });
});
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
new file mode 100644
index 00000000000..3d7c3c839da
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount } from '@vue/test-utils';
+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';
+
+describe('Pipeline Status', () => {
+ let wrapper;
+
+ const createComponent = ({ hasStages = true } = {}) => {
+ wrapper = shallowMount(PipelineEditorMiniGraph, {
+ propsData: {
+ pipeline: mockProjectPipeline({ hasStages }).pipeline,
+ },
+ });
+ };
+
+ const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when there are stages', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders pipeline mini graph', () => {
+ expect(findPipelineMiniGraph().exists()).toBe(true);
+ });
+ });
+
+ describe('when there are no stages', () => {
+ beforeEach(() => {
+ createComponent({ hasStages: false });
+ });
+
+ it('does not render pipeline mini graph', () => {
+ expect(findPipelineMiniGraph().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index b019bae886c..8e0a73b6e7c 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -6,9 +6,6 @@ import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_e
describe('Pipeline editor empty state', () => {
let wrapper;
const defaultProvide = {
- glFeatures: {
- pipelineEditorEmptyStateAction: false,
- },
emptyStateIllustrationPath: 'my/svg/path',
};
@@ -51,24 +48,6 @@ describe('Pipeline editor empty state', () => {
expect(findFileNav().exists()).toBe(true);
});
- describe('with feature flag off', () => {
- it('does not renders a CTA button', () => {
- expect(findConfirmButton().exists()).toBe(false);
- });
- });
- });
-
- describe('with feature flag on', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: {
- pipelineEditorEmptyStateAction: true,
- },
- },
- });
- });
-
it('renders a CTA button', () => {
expect(findConfirmButton().exists()).toBe(true);
expect(findConfirmButton().text()).toBe(wrapper.vm.$options.i18n.btnText);
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index f2104f25324..0b0ff14486e 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -247,20 +247,47 @@ export const mockEmptySearchBranches = {
export const mockBranchPaginationLimit = 10;
export const mockTotalBranches = 20; // must be greater than mockBranchPaginationLimit to test pagination
-export const mockProjectPipeline = {
- pipeline: {
- commitPath: '/-/commit/aabbccdd',
- id: 'gid://gitlab/Ci::Pipeline/118',
- iid: '28',
- shortSha: mockCommitSha,
- status: 'SUCCESS',
- detailedStatus: {
- detailsPath: '/root/sample-ci-project/-/pipelines/118"',
- group: 'success',
- icon: 'status_success',
- text: 'passed',
+export const mockProjectPipeline = ({ hasStages = true } = {}) => {
+ const stages = hasStages
+ ? {
+ edges: [
+ {
+ node: {
+ id: 'gid://gitlab/Ci::Stage/605',
+ name: 'prepare',
+ status: 'success',
+ detailedStatus: {
+ detailsPath: '/root/sample-ci-project/-/pipelines/268#prepare',
+ group: 'success',
+ hasDetails: true,
+ icon: 'status_success',
+ id: 'success-605-605',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ },
+ ],
+ }
+ : null;
+
+ return {
+ pipeline: {
+ commitPath: '/-/commit/aabbccdd',
+ id: 'gid://gitlab/Ci::Pipeline/118',
+ iid: '28',
+ shortSha: mockCommitSha,
+ status: 'SUCCESS',
+ detailedStatus: {
+ detailsPath: '/root/sample-ci-project/-/pipelines/118',
+ group: 'success',
+ icon: 'status_success',
+ text: 'passed',
+ },
+ stages,
},
- },
+ };
};
export const mockLintResponse = {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 393cad0546b..b6713319e69 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -22,7 +22,6 @@ import {
mockCiConfigPath,
mockCiConfigQueryResponse,
mockBlobContentQueryResponse,
- mockBlobContentQueryResponseEmptyCiFile,
mockBlobContentQueryResponseNoCiFile,
mockCiYml,
mockCommitSha,
@@ -43,9 +42,6 @@ const MockSourceEditor = {
const mockProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
- glFeatures: {
- pipelineEditorEmptyStateAction: false,
- },
projectFullPath: mockProjectFullPath,
};
@@ -221,37 +217,12 @@ describe('Pipeline editor app component', () => {
});
});
- describe('with an empty CI config file', () => {
- describe('with empty state feature flag on', () => {
- it('does not show the empty screen state', async () => {
- mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseEmptyCiFile);
-
- await createComponentWithApollo({
- provide: {
- glFeatures: {
- pipelineEditorEmptyStateAction: true,
- },
- },
- });
-
- expect(findEmptyState().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(true);
- });
- });
- });
-
- describe('when landing on the empty state with feature flag on', () => {
- it('user can click on CTA button and see an empty editor', async () => {
+ describe('with no CI config setup', () => {
+ it('user can click on CTA button to get started', async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
- await createComponentWithApollo({
- provide: {
- glFeatures: {
- pipelineEditorEmptyStateAction: true,
- },
- },
- });
+ await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 7aba336b8e8..335049892ec 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -25,7 +25,6 @@ describe('Pipeline editor home wrapper', () => {
},
provide: {
glFeatures: {
- pipelineEditorDrawer: true,
...glFeatures,
},
},
@@ -94,12 +93,4 @@ describe('Pipeline editor home wrapper', () => {
expect(findCommitSection().exists()).toBe(true);
});
});
-
- describe('Pipeline drawer', () => {
- it('hides the drawer when the feature flag is off', () => {
- createComponent({ glFeatures: { pipelineEditorDrawer: false } });
-
- expect(findPipelineEditorDrawer().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js
index 154828aff4b..1cb43c199aa 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
-const { pipelines } = getJSONFixture('pipelines/pipelines.json');
const mockStages = pipelines[0].details.stages;
describe('Pipeline Mini Graph', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js
new file mode 100644
index 00000000000..249126390f1
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
+import PipelineStopModal from '~/pipelines/components/pipelines_list/pipeline_stop_modal.vue';
+import { mockPipelineHeader } from '../../mock_data';
+
+describe('PipelineStopModal', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineStopModal, {
+ propsData: {
+ pipeline: mockPipelineHeader,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render "stop pipeline" warning', () => {
+ expect(wrapper.text()).toMatch(`You’re about to stop pipeline #${mockPipelineHeader.id}.`);
+ });
+});
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index cbc5d11403e..06f1fa4c827 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -205,4 +205,64 @@ describe('pipeline graph job item', () => {
},
);
});
+
+ describe('job classes', () => {
+ it('job class is shown', () => {
+ createWrapper({
+ job: mockJob,
+ cssClassJobName: 'my-class',
+ });
+
+ expect(wrapper.find('a').classes()).toContain('my-class');
+
+ expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ });
+
+ it('job class is shown, along with hover', () => {
+ createWrapper({
+ job: mockJob,
+ cssClassJobName: 'my-class',
+ sourceJobHovered: mockJob.name,
+ });
+
+ expect(wrapper.find('a').classes()).toContain('my-class');
+ expect(wrapper.find('a').classes()).toContain(triggerActiveClass);
+ });
+
+ it('multiple job classes are shown', () => {
+ createWrapper({
+ job: mockJob,
+ cssClassJobName: ['my-class-1', 'my-class-2'],
+ });
+
+ expect(wrapper.find('a').classes()).toContain('my-class-1');
+ expect(wrapper.find('a').classes()).toContain('my-class-2');
+
+ expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ });
+
+ it('multiple job classes are shown conditionally', () => {
+ createWrapper({
+ job: mockJob,
+ cssClassJobName: { 'my-class-1': true, 'my-class-2': true },
+ });
+
+ expect(wrapper.find('a').classes()).toContain('my-class-1');
+ expect(wrapper.find('a').classes()).toContain('my-class-2');
+
+ expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ });
+
+ it('multiple job classes are shown, along with a hover', () => {
+ createWrapper({
+ job: mockJob,
+ cssClassJobName: ['my-class-1', 'my-class-2'],
+ sourceJobHovered: mockJob.name,
+ });
+
+ expect(wrapper.find('a').classes()).toContain('my-class-1');
+ expect(wrapper.find('a').classes()).toContain('my-class-2');
+ expect(wrapper.find('a').classes()).toContain(triggerActiveClass);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index a606595b37d..e24d2e51f08 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -95,7 +95,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
createComponent({ mockData: { artifacts } });
expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path);
- expect(findFirstArtifactItem().text()).toBe(`Download ${artifacts[0].name} artifact`);
+ expect(findFirstArtifactItem().text()).toBe(artifacts[0].name);
});
it('should render empty message when no artifacts are found', () => {
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index 336255768d7..f33c66dedf3 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -87,8 +87,7 @@ describe('Pipelines Artifacts dropdown', () => {
createComponent({ mockData: { artifacts } });
expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path);
-
- expect(findFirstGlDropdownItem().text()).toBe(`Download ${artifacts[0].name} artifact`);
+ expect(findFirstGlDropdownItem().text()).toBe(artifacts[0].name);
});
describe('with a failing request', () => {
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index aa30062c987..2875498bb52 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash';
import { nextTick } from 'vue';
+import mockPipelinesResponse from 'test_fixtures/pipelines/pipelines.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -33,7 +34,6 @@ jest.mock('~/experimentation/utils', () => ({
const mockProjectPath = 'twitter/flight';
const mockProjectId = '21';
const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`;
-const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json');
const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id);
const mockPipelineWithStages = mockPipelinesResponse.pipelines.find(
(p) => p.details.stages && p.details.stages.length,
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 4472a5ae70d..fb019b463b1 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -1,6 +1,7 @@
import '~/commons';
import { GlTable } 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';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
@@ -20,8 +21,6 @@ describe('Pipelines Table', () => {
let pipeline;
let wrapper;
- const jsonFixtureName = 'pipelines/pipelines.json';
-
const defaultProps = {
pipelines: [],
viewType: 'root',
@@ -29,7 +28,8 @@ describe('Pipelines Table', () => {
};
const createMockPipeline = () => {
- const { pipelines } = getJSONFixture(jsonFixtureName);
+ // Clone fixture as it could be modified by tests
+ const { pipelines } = JSON.parse(JSON.stringify(fixture));
return pipelines.find((p) => p.user !== null && p.commit !== null);
};
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index e931ddb8496..84a9f4776b9 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import { getJSONFixture } from 'helpers/fixtures';
+import testReports from 'test_fixtures/pipelines/test_report.json';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
@@ -13,7 +13,6 @@ describe('Actions TestReports Store', () => {
let mock;
let state;
- const testReports = getJSONFixture('pipelines/test_report.json');
const summary = { total_count: 1 };
const suiteEndpoint = `${TEST_HOST}/tests/suite.json`;
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
index f8298fdaba5..70e3a01dbf1 100644
--- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -1,4 +1,4 @@
-import { getJSONFixture } from 'helpers/fixtures';
+import testReports from 'test_fixtures/pipelines/test_report.json';
import * as getters from '~/pipelines/stores/test_reports/getters';
import {
iconForTestStatus,
@@ -9,8 +9,6 @@ import {
describe('Getters TestReports Store', () => {
let state;
- const testReports = getJSONFixture('pipelines/test_report.json');
-
const defaultState = {
blobPath: '/test/blob/path',
testReports,
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index 191e9e7391c..f2dbeec6a06 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -1,12 +1,10 @@
-import { getJSONFixture } from 'helpers/fixtures';
+import testReports from 'test_fixtures/pipelines/test_report.json';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
import mutations from '~/pipelines/stores/test_reports/mutations';
describe('Mutations TestReports Store', () => {
let mockState;
- const testReports = getJSONFixture('pipelines/test_report.json');
-
const defaultState = {
endpoint: '',
testReports: {},
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index e44d59ba888..384b7cf6930 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import testReports from 'test_fixtures/pipelines/test_report.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EmptyState from '~/pipelines/components/test_reports/empty_state.vue';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
@@ -16,8 +16,6 @@ describe('Test reports app', () => {
let wrapper;
let store;
- const testReports = getJSONFixture('pipelines/test_report.json');
-
const loadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const testsDetail = () => wrapper.findByTestId('tests-detail');
const emptyState = () => wrapper.findComponent(EmptyState);
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index a87145cc557..793bad6b82a 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import testReports from 'test_fixtures/pipelines/test_report.json';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import { TestStatus } from '~/pipelines/constants';
import * as getters from '~/pipelines/stores/test_reports/getters';
@@ -17,7 +17,7 @@ describe('Test reports suite table', () => {
const {
test_suites: [testSuite],
- } = getJSONFixture('pipelines/test_report.json');
+ } = testReports;
testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases];
const testCases = testSuite.test_cases;
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js
index df404d87c99..7eed6671fb9 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
+import testReports from 'test_fixtures/pipelines/test_report.json';
import Summary from '~/pipelines/components/test_reports/test_summary.vue';
import { formattedTime } from '~/pipelines/stores/test_reports/utils';
@@ -8,7 +8,7 @@ describe('Test reports summary', () => {
const {
test_suites: [testSuite],
- } = getJSONFixture('pipelines/test_report.json');
+ } = testReports;
const backButton = () => wrapper.find('.js-back-button');
const totalTests = () => wrapper.find('.js-total-tests');
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
index 892a3742fea..0813739d72f 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -1,6 +1,6 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import testReports from 'test_fixtures/pipelines/test_report.json';
import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
@@ -11,8 +11,6 @@ describe('Test reports summary table', () => {
let wrapper;
let store;
- const testReports = getJSONFixture('pipelines/test_report.json');
-
const allSuitesRows = () => wrapper.findAll('.js-suite-row');
const noSuitesToShow = () => wrapper.find('.js-no-tests-suites');
diff --git a/spec/frontend/pages/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js
index ab8c6d529a8..f6edbab3cca 100644
--- a/spec/frontend/pages/projects/new/components/app_spec.js
+++ b/spec/frontend/projects/new/components/app_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import App from '~/pages/projects/new/components/app.vue';
+import App from '~/projects/new/components/app.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
describe('Experimental new project creation app', () => {
diff --git a/spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
index d4cf8c78600..31ddbc80ae4 100644
--- a/spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js
+++ b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
@@ -1,6 +1,6 @@
import { GlPopover, GlFormInputGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import NewProjectPushTipPopover from '~/pages/projects/new/components/new_project_push_tip_popover.vue';
+import NewProjectPushTipPopover from '~/projects/new/components/new_project_push_tip_popover.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('New project push tip popover', () => {
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
new file mode 100644
index 00000000000..aa16b71172b
--- /dev/null
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -0,0 +1,235 @@
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import eventHub from '~/projects/new/event_hub';
+import NewProjectUrlSelect from '~/projects/new/components/new_project_url_select.vue';
+import searchQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+
+describe('NewProjectUrlSelect component', () => {
+ let wrapper;
+
+ const data = {
+ currentUser: {
+ groups: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/26',
+ fullPath: 'flightjs',
+ },
+ {
+ id: 'gid://gitlab/Group/28',
+ fullPath: 'h5bp',
+ },
+ {
+ id: 'gid://gitlab/Group/30',
+ fullPath: 'h5bp/subgroup',
+ },
+ ],
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespace/1',
+ fullPath: 'root',
+ },
+ },
+ };
+
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const defaultProvide = {
+ namespaceFullPath: 'h5bp',
+ namespaceId: '28',
+ rootUrl: 'https://gitlab.com/',
+ trackLabel: 'blank_project',
+ userNamespaceFullPath: 'root',
+ userNamespaceId: '1',
+ };
+
+ const mountComponent = ({
+ search = '',
+ queryResponse = data,
+ provide = defaultProvide,
+ mountFn = shallowMount,
+ } = {}) => {
+ const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data: queryResponse })]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return mountFn(NewProjectUrlSelect, {
+ localVue,
+ apolloProvider,
+ provide,
+ data() {
+ return {
+ search,
+ };
+ },
+ });
+ };
+
+ const findButtonLabel = () => wrapper.findComponent(GlButton);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findHiddenInput = () => wrapper.find('input');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the root url as a label', () => {
+ wrapper = mountComponent();
+
+ expect(findButtonLabel().text()).toBe(defaultProvide.rootUrl);
+ expect(findButtonLabel().props('label')).toBe(true);
+ });
+
+ describe('when namespaceId is provided', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ it('renders a dropdown with the given namespace full path as the text', () => {
+ expect(findDropdown().props('text')).toBe(defaultProvide.namespaceFullPath);
+ });
+
+ it('renders a dropdown with the given namespace id in the hidden input', () => {
+ expect(findHiddenInput().attributes('value')).toBe(defaultProvide.namespaceId);
+ });
+ });
+
+ describe('when namespaceId is not provided', () => {
+ const provide = {
+ ...defaultProvide,
+ namespaceFullPath: undefined,
+ namespaceId: undefined,
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent({ provide });
+ });
+
+ it("renders a dropdown with the user's namespace full path as the text", () => {
+ expect(findDropdown().props('text')).toBe(defaultProvide.userNamespaceFullPath);
+ });
+
+ it("renders a dropdown with the user's namespace id in the hidden input", () => {
+ expect(findHiddenInput().attributes('value')).toBe(defaultProvide.userNamespaceId);
+ });
+ });
+
+ it('focuses on the input when the dropdown is opened', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ const spy = jest.spyOn(findInput().vm, 'focusInput');
+
+ findDropdown().vm.$emit('shown');
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders expected dropdown items', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems).toHaveLength(6);
+ expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
+ expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[0].fullPath);
+ expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
+ expect(listItems.at(3).text()).toBe(data.currentUser.groups.nodes[2].fullPath);
+ expect(listItems.at(4).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
+ expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath);
+ });
+
+ describe('when selecting from a group template', () => {
+ const groupId = getIdFromGraphQLId(data.currentUser.groups.nodes[1].id);
+
+ beforeEach(async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ eventHub.$emit('select-template', groupId);
+ });
+
+ it('filters the dropdown items to the selected group and children', async () => {
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems).toHaveLength(3);
+ expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
+ expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
+ expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[2].fullPath);
+ });
+
+ it('sets the selection to the group', async () => {
+ expect(findDropdown().props('text')).toBe(data.currentUser.groups.nodes[1].fullPath);
+ });
+ });
+
+ it('renders `No matches found` when there are no matching dropdown items', async () => {
+ const queryResponse = {
+ currentUser: {
+ groups: {
+ nodes: [],
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespace/1',
+ fullPath: 'root',
+ },
+ },
+ };
+
+ 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 () => {
+ wrapper = mountComponent();
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findHiddenInput().attributes()).toMatchObject({
+ name: 'project[namespace_id]',
+ value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
+ });
+ });
+
+ it('tracks clicking on the dropdown', () => {
+ wrapper = mountComponent();
+
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findDropdown().vm.$emit('show');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', {
+ label: defaultProvide.trackLabel,
+ property: 'project_path',
+ });
+
+ unmockTracking();
+ });
+});
diff --git a/spec/frontend/projects/projects_filterable_list_spec.js b/spec/frontend/projects/projects_filterable_list_spec.js
index 377d347623a..d4dbf85b5ca 100644
--- a/spec/frontend/projects/projects_filterable_list_spec.js
+++ b/spec/frontend/projects/projects_filterable_list_spec.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-deprecated
import { getJSONFixture, setHTMLFixture } from 'helpers/fixtures';
import ProjectsFilterableList from '~/projects/projects_filterable_list';
@@ -14,6 +15,7 @@ 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');
diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
new file mode 100644
index 00000000000..a42891423cd
--- /dev/null
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -0,0 +1,345 @@
+import {
+ GlSprintf,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { getUsers, getGroups, getDeployKeys } from '~/projects/settings/api/access_dropdown_api';
+import AccessDropdown, { i18n } from '~/projects/settings/components/access_dropdown.vue';
+import { ACCESS_LEVELS, LEVEL_TYPES } from '~/projects/settings/constants';
+
+jest.mock('~/projects/settings/api/access_dropdown_api', () => ({
+ getGroups: jest.fn().mockResolvedValue({
+ data: [
+ { id: 4, name: 'group4' },
+ { id: 5, name: 'group5' },
+ { id: 6, name: 'group6' },
+ ],
+ }),
+ getUsers: jest.fn().mockResolvedValue({
+ data: [
+ { id: 7, name: 'user7' },
+ { id: 8, name: 'user8' },
+ { id: 9, name: 'user9' },
+ ],
+ }),
+ getDeployKeys: jest.fn().mockResolvedValue({
+ data: [
+ { id: 10, title: 'key10', fingerprint: 'abcdefghijklmnop', owner: { name: 'user1' } },
+ { id: 11, title: 'key11', fingerprint: 'abcdefghijklmnop', owner: { name: 'user2' } },
+ { id: 12, title: 'key12', fingerprint: 'abcdefghijklmnop', owner: { name: 'user3' } },
+ ],
+ }),
+}));
+
+describe('Access Level Dropdown', () => {
+ let wrapper;
+ const mockAccessLevelsData = [
+ {
+ id: 1,
+ text: 'role1',
+ },
+ {
+ id: 2,
+ text: 'role2',
+ },
+ {
+ id: 3,
+ text: 'role3',
+ },
+ ];
+
+ const createComponent = ({
+ accessLevelsData = mockAccessLevelsData,
+ accessLevel = ACCESS_LEVELS.PUSH,
+ hasLicense,
+ label,
+ disabled,
+ preselectedItems,
+ } = {}) => {
+ wrapper = shallowMountExtended(AccessDropdown, {
+ propsData: {
+ accessLevelsData,
+ accessLevel,
+ hasLicense,
+ label,
+ disabled,
+ preselectedItems,
+ },
+ stubs: {
+ GlSprintf,
+ GlDropdown,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownToggleLabel = () => findDropdown().props('text');
+ const findAllDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
+ const findAllDropdownHeaders = () => findDropdown().findAllComponents(GlDropdownSectionHeader);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+ const findDropdownItemWithText = (items, text) =>
+ items.filter((item) => item.text().includes(text)).at(0);
+
+ describe('data request', () => {
+ it('should make an api call for users, groups && deployKeys when user has a license', () => {
+ createComponent();
+ expect(getUsers).toHaveBeenCalled();
+ expect(getGroups).toHaveBeenCalled();
+ expect(getDeployKeys).toHaveBeenCalled();
+ });
+
+ it('should make an api call for deployKeys but not for users or groups when user does not have a license', () => {
+ createComponent({ hasLicense: false });
+ expect(getUsers).not.toHaveBeenCalled();
+ expect(getGroups).not.toHaveBeenCalled();
+ expect(getDeployKeys).toHaveBeenCalled();
+ });
+
+ it('should make api calls when search query is updated', async () => {
+ createComponent();
+ const query = 'root';
+
+ findSearchBox().vm.$emit('input', query);
+ await nextTick();
+ expect(getUsers).toHaveBeenCalledWith(query);
+ expect(getGroups).toHaveBeenCalled();
+ expect(getDeployKeys).toHaveBeenCalledWith(query);
+ });
+ });
+
+ describe('layout', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('renders headers for each section ', () => {
+ expect(findAllDropdownHeaders()).toHaveLength(4);
+ });
+
+ it('renders dropdown item for each access level type', () => {
+ expect(findAllDropdownItems()).toHaveLength(12);
+ });
+ });
+
+ describe('toggleLabel', () => {
+ let dropdownItems = [];
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ dropdownItems = findAllDropdownItems();
+ });
+
+ const findItemByNameAndClick = async (name) => {
+ findDropdownItemWithText(dropdownItems, name).trigger('click');
+ await nextTick();
+ };
+
+ it('when no items selected and custom label provided, displays it and has default CSS class', () => {
+ wrapper.destroy();
+ const customLabel = 'Set the access level';
+ createComponent({ label: customLabel });
+ expect(findDropdownToggleLabel()).toBe(customLabel);
+ expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!');
+ });
+
+ it('when no items selected, displays a default fallback label and has default CSS class ', () => {
+ expect(findDropdownToggleLabel()).toBe(i18n.selectUsers);
+ expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!');
+ });
+
+ it('displays a number of selected items for each group level', async () => {
+ dropdownItems.wrappers.forEach((item) => {
+ item.trigger('click');
+ });
+ await nextTick();
+ expect(findDropdownToggleLabel()).toBe('3 roles, 3 users, 3 deploy keys, 3 groups');
+ });
+
+ it('with only role selected displays the role name and has no class applied', async () => {
+ await findItemByNameAndClick('role1');
+ expect(findDropdownToggleLabel()).toBe('role1');
+ expect(findDropdown().props('toggleClass')).toBe('');
+ });
+
+ it('with only groups selected displays the number of selected groups', async () => {
+ await findItemByNameAndClick('group4');
+ await findItemByNameAndClick('group5');
+ await findItemByNameAndClick('group6');
+ expect(findDropdownToggleLabel()).toBe('3 groups');
+ expect(findDropdown().props('toggleClass')).toBe('');
+ });
+
+ it('with only users selected displays the number of selected users', async () => {
+ await findItemByNameAndClick('user7');
+ await findItemByNameAndClick('user8');
+ expect(findDropdownToggleLabel()).toBe('2 users');
+ expect(findDropdown().props('toggleClass')).toBe('');
+ });
+
+ it('with users and groups selected displays the number of selected users & groups', async () => {
+ await findItemByNameAndClick('group4');
+ await findItemByNameAndClick('group6');
+ await findItemByNameAndClick('user7');
+ await findItemByNameAndClick('user9');
+ expect(findDropdownToggleLabel()).toBe('2 users, 2 groups');
+ expect(findDropdown().props('toggleClass')).toBe('');
+ });
+
+ it('with users and deploy keys selected displays the number of selected users & keys', async () => {
+ await findItemByNameAndClick('user8');
+ await findItemByNameAndClick('key10');
+ await findItemByNameAndClick('key11');
+ expect(findDropdownToggleLabel()).toBe('1 user, 2 deploy keys');
+ expect(findDropdown().props('toggleClass')).toBe('');
+ });
+ });
+
+ describe('selecting an item', () => {
+ it('selects the item on click and deselects on the next click ', async () => {
+ createComponent();
+ await waitForPromises();
+
+ const item = findAllDropdownItems().at(1);
+ item.trigger('click');
+ await nextTick();
+ expect(item.props('isChecked')).toBe(true);
+ item.trigger('click');
+ await nextTick();
+ expect(item.props('isChecked')).toBe(false);
+ });
+
+ it('emits a formatted update on selection ', async () => {
+ // ids: the items appear in that order in the dropdown
+ // 1 2 3 - roles
+ // 4 5 6 - groups
+ // 7 8 9 - users
+ // 10 11 12 - deploy_keys
+ // we set 2 from each group as preselected. Then for the sake of the test deselect one, leave one as-is
+ // and select a new one from the group.
+ // Preselected items should have `id` along with `user_id/group_id/access_level/deplo_key_id`.
+ // Items to be removed from previous selection will have `_deploy` flag set to true
+ // Newly selected items will have only `user_id/group_id/access_level/deploy_key_id` (depending on their type);
+ const preselectedItems = [
+ { id: 112, type: 'role', access_level: 2 },
+ { id: 113, type: 'role', access_level: 3 },
+ { id: 115, type: 'group', group_id: 5 },
+ { id: 116, type: 'group', group_id: 6 },
+ { id: 118, type: 'user', user_id: 8, name: 'user8' },
+ { id: 119, type: 'user', user_id: 9, name: 'user9' },
+ { id: 121, type: 'deploy_key', deploy_key_id: 11 },
+ { id: 122, type: 'deploy_key', deploy_key_id: 12 },
+ ];
+
+ createComponent({ preselectedItems });
+ await waitForPromises();
+ const spy = jest.spyOn(wrapper.vm, '$emit');
+ const dropdownItems = findAllDropdownItems();
+ // select new item from each group
+ findDropdownItemWithText(dropdownItems, 'role1').trigger('click');
+ findDropdownItemWithText(dropdownItems, 'group4').trigger('click');
+ findDropdownItemWithText(dropdownItems, 'user7').trigger('click');
+ findDropdownItemWithText(dropdownItems, 'key10').trigger('click');
+ // deselect one item from each group
+ findDropdownItemWithText(dropdownItems, 'role2').trigger('click');
+ findDropdownItemWithText(dropdownItems, 'group5').trigger('click');
+ findDropdownItemWithText(dropdownItems, 'user8').trigger('click');
+ findDropdownItemWithText(dropdownItems, 'key11').trigger('click');
+
+ expect(spy).toHaveBeenLastCalledWith('select', [
+ { access_level: 1 },
+ { id: 112, access_level: 2, _destroy: true },
+ { id: 113, access_level: 3 },
+ { group_id: 4 },
+ { id: 115, group_id: 5, _destroy: true },
+ { id: 116, group_id: 6 },
+ { user_id: 7 },
+ { id: 118, user_id: 8, _destroy: true },
+ { id: 119, user_id: 9 },
+ { deploy_key_id: 10 },
+ { id: 121, deploy_key_id: 11, _destroy: true },
+ { id: 122, deploy_key_id: 12 },
+ ]);
+ });
+ });
+
+ describe('Handling preselected items', () => {
+ const preselectedItems = [
+ { id: 112, type: 'role', access_level: 2 },
+ { id: 115, type: 'group', group_id: 5 },
+ { id: 118, type: 'user', user_id: 8, name: 'user2' },
+ { id: 121, type: 'deploy_key', deploy_key_id: 11 },
+ ];
+
+ const findSelected = (type) =>
+ wrapper.findAllByTestId(`${type}-dropdown-item`).filter((w) => w.props('isChecked'));
+
+ beforeEach(async () => {
+ createComponent({ preselectedItems });
+ await waitForPromises();
+ });
+
+ it('should set selected roles as intersection between the server response and preselected', () => {
+ const selectedRoles = findSelected(LEVEL_TYPES.ROLE);
+ expect(selectedRoles).toHaveLength(1);
+ expect(selectedRoles.at(0).text()).toBe('role2');
+ });
+
+ it('should set selected groups as intersection between the server response and preselected', () => {
+ const selectedGroups = findSelected(LEVEL_TYPES.GROUP);
+ expect(selectedGroups).toHaveLength(1);
+ expect(selectedGroups.at(0).text()).toBe('group5');
+ });
+
+ it('should set selected users to all preselected mapping `user_id` to `id`', () => {
+ const selectedUsers = findSelected(LEVEL_TYPES.USER);
+ expect(selectedUsers).toHaveLength(1);
+ expect(selectedUsers.at(0).text()).toBe('user2');
+ });
+
+ it('should set selected deploy keys as intersection between the server response and preselected mapping some keys', () => {
+ const selectedDeployKeys = findSelected(LEVEL_TYPES.DEPLOY_KEY);
+ expect(selectedDeployKeys).toHaveLength(1);
+ expect(selectedDeployKeys.at(0).text()).toContain('key11 (abcdefghijklmn...)');
+ });
+ });
+
+ describe('on dropdown open', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should set the search input focus', () => {
+ wrapper.vm.$refs.search.focusInput = jest.fn();
+ findDropdown().vm.$emit('shown');
+
+ expect(wrapper.vm.$refs.search.focusInput).toHaveBeenCalled();
+ });
+ });
+
+ describe('on dropdown close', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should emit `hidden` event with dropdown selection', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findAllDropdownItems().at(1).trigger('click');
+
+ findDropdown().vm.$emit('hidden');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('hidden', [{ access_level: 2 }]);
+ });
+ });
+});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index a642a8cf8c2..b486992ac4b 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -4,6 +4,9 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { merge, last } from 'lodash';
import Vuex from 'vuex';
+import commit from 'test_fixtures/api/commits/commit.json';
+import branches from 'test_fixtures/api/branches/branches.json';
+import tags from 'test_fixtures/api/tags/tags.json';
import { trimText } from 'helpers/text_helper';
import { ENTER_KEY } from '~/lib/utils/keys';
import { sprintf } from '~/locale';
@@ -21,11 +24,7 @@ const localVue = createLocalVue();
localVue.use(Vuex);
describe('Ref selector component', () => {
- const fixtures = {
- branches: getJSONFixture('api/branches/branches.json'),
- tags: getJSONFixture('api/tags/tags.json'),
- commit: getJSONFixture('api/commits/commit.json'),
- };
+ const fixtures = { branches, tags, commit };
const projectId = '8';
@@ -480,8 +479,6 @@ describe('Ref selector component', () => {
it('renders each commit as a selectable item with the short SHA and commit title', () => {
const dropdownItems = findCommitDropdownItems();
- const { commit } = fixtures;
-
expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`);
});
});
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index c8fcb3116cd..a5da37a2786 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -1,13 +1,12 @@
-import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui';
+import { GlFormCheckbox, GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import DeleteButton from '~/registry/explorer/components/delete_button.vue';
+
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
- REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
MISSING_MANIFEST_WARNING_TOOLTIP,
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
@@ -25,19 +24,20 @@ describe('tags list row', () => {
const defaultProps = { tag, isMobile: false, index: 0 };
- const findCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findName = () => wrapper.find('[data-testid="name"]');
const findSize = () => wrapper.find('[data-testid="size"]');
const findTime = () => wrapper.find('[data-testid="time"]');
const findShortRevision = () => wrapper.find('[data-testid="digest"]');
- const findClipboardButton = () => wrapper.find(ClipboardButton);
- const findDeleteButton = () => wrapper.find(DeleteButton);
- const findTimeAgoTooltip = () => wrapper.find(TimeAgoTooltip);
+ const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
+ const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const findDetailsRows = () => wrapper.findAll(DetailsRow);
const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]');
const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]');
const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]');
- const findWarningIcon = () => wrapper.find(GlIcon);
+ const findWarningIcon = () => wrapper.findComponent(GlIcon);
+ const findAdditionalActionsMenu = () => wrapper.findComponent(GlDropdown);
+ const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, {
@@ -45,6 +45,7 @@ describe('tags list row', () => {
GlSprintf,
ListItem,
DetailsRow,
+ GlDropdown,
},
propsData,
directives: {
@@ -262,44 +263,61 @@ describe('tags list row', () => {
});
});
- describe('delete button', () => {
+ describe('additional actions menu', () => {
it('exists', () => {
mountComponent();
- expect(findDeleteButton().exists()).toBe(true);
+ expect(findAdditionalActionsMenu().exists()).toBe(true);
});
- it('has the correct props/attributes', () => {
+ it('has the correct props', () => {
mountComponent();
- expect(findDeleteButton().attributes()).toMatchObject({
- title: REMOVE_TAG_BUTTON_TITLE,
- tooltiptitle: REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
- tooltipdisabled: 'true',
+ expect(findAdditionalActionsMenu().props()).toMatchObject({
+ icon: 'ellipsis_v',
+ text: 'More actions',
+ textSrOnly: true,
+ category: 'tertiary',
+ right: true,
});
});
it.each`
- canDelete | digest | disabled
- ${true} | ${null} | ${true}
- ${false} | ${'foo'} | ${true}
- ${false} | ${null} | ${true}
- ${true} | ${'foo'} | ${true}
+ canDelete | digest | disabled | buttonDisabled
+ ${true} | ${null} | ${true} | ${true}
+ ${false} | ${'foo'} | ${true} | ${true}
+ ${false} | ${null} | ${true} | ${true}
+ ${true} | ${'foo'} | ${true} | ${true}
+ ${true} | ${'foo'} | ${false} | ${false}
`(
- 'is disabled when canDelete is $canDelete and digest is $digest and disabled is $disabled',
- ({ canDelete, digest, disabled }) => {
+ 'is $visible that is visible when canDelete is $canDelete and digest is $digest and disabled is $disabled',
+ ({ canDelete, digest, disabled, buttonDisabled }) => {
mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled });
- expect(findDeleteButton().attributes('disabled')).toBe('true');
+ expect(findAdditionalActionsMenu().props('disabled')).toBe(buttonDisabled);
+ expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(buttonDisabled);
+ expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(buttonDisabled);
},
);
- it('delete event emits delete', () => {
- mountComponent();
+ describe('delete button', () => {
+ it('exists and has the correct attrs', () => {
+ mountComponent();
+
+ expect(findDeleteButton().exists()).toBe(true);
+ expect(findDeleteButton().attributes()).toMatchObject({
+ variant: 'danger',
+ });
+ expect(findDeleteButton().text()).toBe(REMOVE_TAG_BUTTON_TITLE);
+ });
- findDeleteButton().vm.$emit('delete');
+ it('delete event emits delete', () => {
+ mountComponent();
- expect(wrapper.emitted('delete')).toEqual([[]]);
+ findDeleteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
});
});
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index b58a53f0af2..e1f24a2b65b 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -129,13 +129,16 @@ describe('List Page', () => {
});
});
- describe('connection error', () => {
+ describe.each([
+ { error: 'connectionError', errorName: 'connection error' },
+ { error: 'invalidPathError', errorName: 'invalid path error' },
+ ])('handling $errorName', ({ error }) => {
const config = {
- characterError: true,
containersErrorImage: 'foo',
helpPagePath: 'bar',
isGroupPage: false,
};
+ config[error] = true;
it('should show an empty state', () => {
mountComponent({ config });
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 f306fdef624..67f62815720 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
@@ -1,23 +1,19 @@
import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import mockData from 'test_fixtures/issues/related_merge_requests.json';
import axios from '~/lib/utils/axios_utils';
import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
import createStore from '~/related_merge_requests/store/index';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-const FIXTURE_PATH = 'issues/related_merge_requests.json';
const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
const localVue = createLocalVue();
describe('RelatedMergeRequests', () => {
let wrapper;
let mock;
- let mockData;
beforeEach((done) => {
- loadFixtures(FIXTURE_PATH);
- mockData = getJSONFixture(FIXTURE_PATH);
-
// put the fixture in DOM as the component expects
document.body.innerHTML = `<div id="js-issuable-app"></div>`;
document.getElementById('js-issuable-app').dataset.initial = JSON.stringify(mockData);
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 1db6fa21d6b..029d720f7b9 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -3,7 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as commonUtils from '~/lib/utils/common_utils';
@@ -11,7 +11,6 @@ import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
-const originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
index 096d319c82f..32bbfd386f5 100644
--- a/spec/frontend/releases/components/app_index_apollo_client_spec.js
+++ b/spec/frontend/releases/components/app_index_apollo_client_spec.js
@@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
@@ -32,9 +33,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
describe('app_index_apollo_client.vue', () => {
- const originalAllReleasesQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
- );
const projectPath = 'project/path';
const newReleasePath = 'path/to/new/release/page';
const before = 'beforeCursor';
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index 7ea7a6ffe94..72ebaaaf76c 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { getJSONFixture } from 'helpers/fixtures';
+import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import createFlash from '~/flash';
import ReleaseShowApp from '~/releases/components/app_show.vue';
@@ -11,10 +11,6 @@ import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphq
jest.mock('~/flash');
-const oneReleaseQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release.query.graphql.json',
-);
-
Vue.use(VueApollo);
const EXPECTED_ERROR_MESSAGE = 'Something went wrong while getting the release details.';
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 460007e48ef..839d127e00f 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -1,6 +1,6 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import * as commonUtils from '~/lib/utils/common_utils';
import { ENTER_KEY } from '~/lib/utils/keys';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
@@ -9,8 +9,6 @@ import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release edit component', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 50b6d1c4707..973428257b7 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -1,13 +1,11 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Evidence Block', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index 3b9b16fa890..c63689e11ac 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -1,13 +1,11 @@
import { GlCollapse } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
+import { assets } from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
-const { assets } = getJSONFixture('api/releases/release.json');
-
describe('Release block assets', () => {
let wrapper;
let defaultProps;
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index e9fa22b4ec7..f645dc309d7 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,13 +1,11 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31;
const mockFutureDate = new Date(new Date().getTime() + MONTHS_IN_MS).toISOString();
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index 47fd6377fcf..167ae4f32a2 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -1,14 +1,12 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import setWindowLocation from 'helpers/set_window_location_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release block header', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index a2bf45c7861..146b2cc7490 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -1,12 +1,12 @@
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants';
-const { milestones: originalMilestones } = getJSONFixture('api/releases/release.json');
+const { milestones: originalMilestones } = originalRelease;
describe('Release block milestone info', () => {
let wrapper;
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 1ca441f7a5a..a847c32b8f1 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import $ from 'jquery';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
@@ -9,8 +9,6 @@ import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release block', () => {
let wrapper;
let release;
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 6504a09df2f..d8329fb82b1 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -27,10 +27,6 @@ jest.mock('~/releases/util', () => ({
},
}));
-const originalOneReleaseForEditingQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json',
-);
-
describe('Release edit/new actions', () => {
let state;
let releaseResponse;
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 20ae332e500..24dcedb3580 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -1,12 +1,10 @@
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
import * as types from '~/releases/stores/modules/edit_new/mutation_types';
import mutations from '~/releases/stores/modules/edit_new/mutations';
import createState from '~/releases/stores/modules/edit_new/state';
-const originalRelease = getJSONFixture('api/releases/release.json');
-
describe('Release edit/new mutations', () => {
let state;
let release;
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
index af520c2eb20..91406f7e2f4 100644
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/list/actions_spec.js
@@ -1,5 +1,5 @@
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalGraphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import { PAGE_SIZE } from '~/releases/constants';
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
@@ -12,10 +12,6 @@ import * as types from '~/releases/stores/modules/index/mutation_types';
import createState from '~/releases/stores/modules/index/state';
import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
-const originalGraphqlReleasesResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
-);
-
describe('Releases State actions', () => {
let mockedState;
let graphqlReleasesResponse;
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
index 08d803b3c2c..49e324c28a5 100644
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/list/mutations_spec.js
@@ -1,17 +1,13 @@
-import { getJSONFixture } from 'helpers/fixtures';
+import originalRelease from 'test_fixtures/api/releases/release.json';
+import graphqlReleasesResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from '~/releases/stores/modules/index/mutation_types';
import mutations from '~/releases/stores/modules/index/mutations';
import createState from '~/releases/stores/modules/index/state';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
-const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
-const graphqlReleasesResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
-);
-
describe('Releases Store Mutations', () => {
let stateCopy;
let pageInfo;
diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js
index 36e7be369d3..3c1060cb0e8 100644
--- a/spec/frontend/releases/util_spec.js
+++ b/spec/frontend/releases/util_spec.js
@@ -1,21 +1,13 @@
import { cloneDeep } from 'lodash';
-import { getJSONFixture } from 'helpers/fixtures';
+import originalAllReleasesQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/all_releases.query.graphql.json';
+import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
+import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import {
convertGraphQLRelease,
convertAllReleasesGraphQLResponse,
convertOneReleaseGraphQLResponse,
} from '~/releases/util';
-const originalAllReleasesQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
-);
-const originalOneReleaseQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release.query.graphql.json',
-);
-const originalOneReleaseForEditingQueryResponse = getJSONFixture(
- 'graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json',
-);
-
describe('releases/util.js', () => {
describe('convertGraphQLRelease', () => {
let releaseFromResponse;
diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
index f99dcbffdff..c548007a8a6 100644
--- a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -38,6 +38,12 @@ describe('code quality issue body issue body', () => {
describe('severity rating', () => {
it.each`
severity | iconClass | iconName
+ ${'INFO'} | ${'text-primary-400'} | ${'severity-info'}
+ ${'MINOR'} | ${'text-warning-200'} | ${'severity-low'}
+ ${'CRITICAL'} | ${'text-danger-600'} | ${'severity-high'}
+ ${'BLOCKER'} | ${'text-danger-800'} | ${'severity-critical'}
+ ${'UNKNOWN'} | ${'text-secondary-400'} | ${'severity-unknown'}
+ ${'INVALID'} | ${'text-secondary-400'} | ${'severity-unknown'}
${'info'} | ${'text-primary-400'} | ${'severity-info'}
${'minor'} | ${'text-warning-200'} | ${'severity-low'}
${'major'} | ${'text-warning-400'} | ${'severity-medium'}
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
index 84863eac3d3..685a1c50a46 100644
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -60,7 +60,7 @@ describe('Grouped code quality reports app', () => {
});
it('should render loading text', () => {
- expect(findWidget().text()).toEqual('Loading codeclimate report');
+ expect(findWidget().text()).toEqual('Loading Code quality report');
});
});
@@ -84,7 +84,7 @@ describe('Grouped code quality reports app', () => {
});
it('renders summary text', () => {
- expect(findWidget().text()).toContain('Code quality degraded on 1 point');
+ expect(findWidget().text()).toContain('Code quality degraded');
});
it('renders custom codequality issue body', () => {
@@ -99,7 +99,7 @@ describe('Grouped code quality reports app', () => {
});
it('renders summary text', () => {
- expect(findWidget().text()).toContain('Code quality improved on 1 point');
+ expect(findWidget().text()).toContain('Code quality improved');
});
it('renders custom codequality issue body', () => {
@@ -115,7 +115,7 @@ describe('Grouped code quality reports app', () => {
it('renders summary text', () => {
expect(findWidget().text()).toContain(
- 'Code quality improved on 1 point and degraded on 1 point',
+ 'Code quality scanning detected 2 changes in merged results',
);
});
@@ -132,7 +132,7 @@ describe('Grouped code quality reports app', () => {
});
it('renders error text', () => {
- expect(findWidget().text()).toContain('Failed to load codeclimate report');
+ expect(findWidget().text()).toContain('Failed to load Code quality report');
});
it('does not render a help icon', () => {
diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/reports/codequality_report/store/getters_spec.js
index 0378171084d..b5f6edf85eb 100644
--- a/spec/frontend/reports/codequality_report/store/getters_spec.js
+++ b/spec/frontend/reports/codequality_report/store/getters_spec.js
@@ -61,9 +61,9 @@ describe('Codequality reports store getters', () => {
it.each`
resolvedIssues | newIssues | expectedText
${0} | ${0} | ${'No changes to code quality'}
- ${0} | ${1} | ${'Code quality degraded on 1 point'}
- ${2} | ${0} | ${'Code quality improved on 2 points'}
- ${1} | ${2} | ${'Code quality improved on 1 point and degraded on 2 points'}
+ ${0} | ${1} | ${'Code quality degraded'}
+ ${2} | ${0} | ${'Code quality improved'}
+ ${1} | ${2} | ${'Code quality scanning detected 3 changes in merged results'}
`(
'returns a summary containing $resolvedIssues resolved issues and $newIssues new issues',
({ newIssues, resolvedIssues, expectedText }) => {
diff --git a/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js b/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js
index ba95294ab0a..5b77a2c74be 100644
--- a/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js
+++ b/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js
@@ -25,6 +25,18 @@ describe('Codequality report store utils', () => {
});
});
+ describe('when an issue has a non-nested path', () => {
+ const issue = { description: 'Insecure Dependency', path: 'Gemfile.lock' };
+
+ beforeEach(() => {
+ [result] = parseCodeclimateMetrics([issue], 'path');
+ });
+
+ it('is parsed', () => {
+ expect(result.name).toEqual(issue.description);
+ });
+ });
+
describe('when an issue has a path but no line', () => {
const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } };
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index e1b36aa1e21..39932b62dbb 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -23,7 +23,7 @@ describe('Report section', () => {
const defaultProps = {
component: '',
status: 'SUCCESS',
- loadingText: 'Loading codeclimate report',
+ loadingText: 'Loading Code Quality report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues,
@@ -117,13 +117,13 @@ describe('Report section', () => {
vm = mountComponent(ReportSection, {
component: '',
status: 'LOADING',
- loadingText: 'Loading codeclimate report',
+ loadingText: 'Loading Code Quality report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
hasIssues: false,
});
- expect(vm.$el.textContent.trim()).toEqual('Loading codeclimate report');
+ expect(vm.$el.textContent.trim()).toEqual('Loading Code Quality report');
});
});
@@ -229,13 +229,13 @@ describe('Report section', () => {
vm = mountComponent(ReportSection, {
component: '',
status: 'ERROR',
- loadingText: 'Loading codeclimate report',
- errorText: 'Failed to load codeclimate report',
+ loadingText: 'Loading Code Quality report',
+ errorText: 'Failed to load Code Quality report',
successText: 'Code quality improved on 1 point and degraded on 1 point',
hasIssues: false,
});
- expect(vm.$el.textContent.trim()).toEqual('Failed to load codeclimate report');
+ expect(vm.$el.textContent.trim()).toEqual('Failed to load Code Quality report');
});
});
diff --git a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
index 0f7c2559e8b..c60c1f7b63c 100644
--- a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
+++ b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
@@ -24,7 +24,7 @@ describe('Grouped test reports app', () => {
let wrapper;
let mockStore;
- const mountComponent = ({ props = { pipelinePath }, glFeatures = {} } = {}) => {
+ const mountComponent = ({ props = { pipelinePath } } = {}) => {
wrapper = mount(GroupedTestReportsApp, {
store: mockStore,
localVue,
@@ -34,9 +34,6 @@ describe('Grouped test reports app', () => {
pipelinePath,
...props,
},
- provide: {
- glFeatures,
- },
});
};
@@ -114,8 +111,8 @@ describe('Grouped test reports app', () => {
setReports(newFailedTestReports);
});
- it('tracks service ping metric when enabled', () => {
- mountComponent({ glFeatures: { usageDataITestingSummaryWidgetTotal: true } });
+ it('tracks service ping metric', () => {
+ mountComponent();
findExpandButton().trigger('click');
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
@@ -123,7 +120,7 @@ describe('Grouped test reports app', () => {
});
it('only tracks the first expansion', () => {
- mountComponent({ glFeatures: { usageDataITestingSummaryWidgetTotal: true } });
+ mountComponent();
const expandButton = findExpandButton();
expandButton.trigger('click');
expandButton.trigger('click');
@@ -131,13 +128,6 @@ describe('Grouped test reports app', () => {
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
});
-
- it('does not track service ping metric when disabled', () => {
- mountComponent({ glFeatures: { usageDataITestingSummaryWidgetTotal: false } });
- findExpandButton().trigger('click');
-
- expect(Api.trackRedisHllUserEvent).not.toHaveBeenCalled();
- });
});
describe('with new failed result', () => {
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
new file mode 100644
index 00000000000..d924974aede
--- /dev/null
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -0,0 +1,84 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
+import httpStatus from '~/lib/utils/http_status';
+import createFlash from '~/flash';
+import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
+
+jest.mock('~/flash');
+
+describe('commits service', () => {
+ let mock;
+ const url = `${gon.relative_url_root || ''}/my-project/-/refs/main/logs_tree/`;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(url).reply(httpStatus.OK, [], {});
+
+ jest.spyOn(axios, 'get');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ resetRequestedCommits();
+ });
+
+ const requestCommits = (offset, project = 'my-project', path = '', ref = 'main') =>
+ loadCommits(project, path, ref, offset);
+
+ it('calls axios get', async () => {
+ const offset = 10;
+ const project = 'my-project';
+ const path = 'my-path';
+ const ref = 'my-ref';
+ const testUrl = `${gon.relative_url_root || ''}/${project}/-/refs/${ref}/logs_tree/${path}`;
+
+ await requestCommits(offset, project, path, ref);
+
+ expect(axios.get).toHaveBeenCalledWith(testUrl, { params: { format: 'json', offset } });
+ });
+
+ it('encodes the path correctly', async () => {
+ await requestCommits(1, 'some-project', 'with $peci@l ch@rs/');
+
+ const encodedUrl = '/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F';
+ expect(axios.get).toHaveBeenCalledWith(encodedUrl, expect.anything());
+ });
+
+ it('calls axios get once per batch', async () => {
+ await Promise.all([requestCommits(0), requestCommits(1), requestCommits(23)]);
+
+ expect(axios.get.mock.calls.length).toEqual(1);
+ });
+
+ it('calls axios get twice if an offset is larger than 25', async () => {
+ await requestCommits(100);
+
+ expect(axios.get.mock.calls[0][1]).toEqual({ params: { format: 'json', offset: 75 } });
+ expect(axios.get.mock.calls[1][1]).toEqual({ params: { format: 'json', offset: 100 } });
+ });
+
+ it('updates the list of requested offsets', async () => {
+ await requestCommits(200);
+
+ expect(isRequested(200)).toBe(true);
+ });
+
+ it('resets the list of requested offsets', async () => {
+ await requestCommits(300);
+
+ resetRequestedCommits();
+ expect(isRequested(300)).toBe(false);
+ });
+
+ it('calls `createFlash` when the request fails', async () => {
+ const invalidPath = '/#@ some/path';
+ const invalidUrl = `${url}${invalidPath}`;
+ mock.onGet(invalidUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, [], {});
+
+ await requestCommits(1, 'my-project', invalidPath);
+
+ expect(createFlash).toHaveBeenCalledWith({ message: I18N_COMMIT_DATA_FETCH_ERROR });
+ });
+});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 8331adcdfc2..59db537282b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -11,13 +11,18 @@ import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
+import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
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';
jest.mock('~/repository/components/blob_viewers');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/common_utils');
let wrapper;
let mockResolver;
@@ -34,12 +39,14 @@ const simpleMockData = {
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',
- forkPath: 'some_file.js/fork',
simpleViewer: {
fileType: 'text',
tooLarge: false,
@@ -62,6 +69,8 @@ const projectMockData = {
userPermissions: {
pushCode: true,
downloadCode: true,
+ createMergeRequestIn: true,
+ forkProject: true,
},
repository: {
empty: false,
@@ -82,6 +91,8 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
emptyRepo = defaultEmptyRepo,
canPushCode = defaultPushCode,
canDownloadCode = defaultDownloadCode,
+ createMergeRequestIn = projectMockData.userPermissions.createMergeRequestIn,
+ forkProject = projectMockData.userPermissions.forkProject,
pathLocks = [],
} = mockData;
@@ -89,7 +100,12 @@ const createComponentWithApollo = (mockData = {}, inject = {}) => {
data: {
project: {
id: '1234',
- userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode },
+ userPermissions: {
+ pushCode: canPushCode,
+ downloadCode: canDownloadCode,
+ createMergeRequestIn,
+ forkProject,
+ },
pathLocks: {
nodes: pathLocks,
},
@@ -158,9 +174,16 @@ describe('Blob content viewer component', () => {
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
+ const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
+
+ beforeEach(() => {
+ gon.features = { refactorTextViewer: true };
+ isLoggedIn.mockReturnValue(true);
+ });
afterEach(() => {
wrapper.destroy();
+ mockAxios.reset();
});
it('renders a GlLoadingIcon component', () => {
@@ -183,7 +206,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
- expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text',
@@ -192,6 +214,16 @@ describe('Blob content viewer component', () => {
renderError: null,
});
});
+
+ describe('legacy viewers', () => {
+ it('loads a legacy viewer when a viewer component is not available', async () => {
+ createComponentWithApollo({ blobs: { ...simpleMockData, fileType: 'unknown' } });
+ await waitForPromises();
+
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
+ });
+ });
});
describe('rich viewer', () => {
@@ -210,7 +242,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
- expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
@@ -241,18 +272,12 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
- it('does not load a legacy viewer when a rich viewer is not available', async () => {
- createComponentWithApollo({ blobs: simpleMockData });
- await waitForPromises();
-
- expect(mockAxios.history.get).toHaveLength(0);
- });
-
- it('loads a legacy viewer when a rich viewer is available', async () => {
- createComponentWithApollo({ blobs: richMockData });
+ it('loads a legacy viewer when a viewer component is not available', async () => {
+ createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
+ expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich');
});
});
@@ -462,7 +487,7 @@ describe('Blob content viewer component', () => {
});
it('does not render if not logged in', async () => {
- window.gon.current_user_id = null;
+ isLoggedIn.mockReturnValueOnce(false);
fullFactory({
mockData: { blobInfo: simpleMockData },
@@ -506,4 +531,60 @@ describe('Blob content viewer component', () => {
);
});
});
+
+ describe('edit blob', () => {
+ beforeEach(() => {
+ fullFactory({
+ mockData: { blobInfo: simpleMockData },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+ });
+
+ it('simple edit redirects to the simple editor', () => {
+ findBlobEdit().vm.$emit('edit', 'simple');
+ expect(redirectTo).toHaveBeenCalledWith(simpleMockData.editBlobPath);
+ });
+
+ it('IDE edit redirects to the IDE editor', () => {
+ findBlobEdit().vm.$emit('edit', 'ide');
+ expect(redirectTo).toHaveBeenCalledWith(simpleMockData.ideEditPath);
+ });
+
+ it.each`
+ loggedIn | canModifyBlob | createMergeRequestIn | forkProject | showForkSuggestion
+ ${true} | ${false} | ${true} | ${true} | ${true}
+ ${false} | ${false} | ${true} | ${true} | ${false}
+ ${true} | ${true} | ${false} | ${true} | ${false}
+ ${true} | ${true} | ${true} | ${false} | ${false}
+ `(
+ 'shows/hides a fork suggestion according to a set of conditions',
+ async ({
+ loggedIn,
+ canModifyBlob,
+ createMergeRequestIn,
+ forkProject,
+ showForkSuggestion,
+ }) => {
+ isLoggedIn.mockReturnValueOnce(loggedIn);
+ fullFactory({
+ mockData: {
+ blobInfo: { ...simpleMockData, canModifyBlob },
+ project: { userPermissions: { createMergeRequestIn, forkProject } },
+ },
+ stubs: {
+ BlobContent: true,
+ BlobButtonGroup: true,
+ },
+ });
+
+ findBlobEdit().vm.$emit('edit', 'simple');
+ await nextTick();
+
+ expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
+ },
+ );
+ });
});
diff --git a/spec/frontend/repository/components/blob_edit_spec.js b/spec/frontend/repository/components/blob_edit_spec.js
index 11739674bc9..e2de7bc2957 100644
--- a/spec/frontend/repository/components/blob_edit_spec.js
+++ b/spec/frontend/repository/components/blob_edit_spec.js
@@ -7,6 +7,7 @@ const DEFAULT_PROPS = {
editPath: 'some_file.js/edit',
webIdePath: 'some_file.js/ide/edit',
showEditButton: true,
+ needsToFork: false,
};
describe('BlobEdit component', () => {
@@ -56,7 +57,6 @@ describe('BlobEdit component', () => {
it('renders the Edit button', () => {
createComponent();
- expect(findEditButton().attributes('href')).toBe(DEFAULT_PROPS.editPath);
expect(findEditButton().text()).toBe('Edit');
expect(findEditButton()).not.toBeDisabled();
});
@@ -64,7 +64,6 @@ describe('BlobEdit component', () => {
it('renders the Web IDE button', () => {
createComponent();
- expect(findWebIdeButton().attributes('href')).toBe(DEFAULT_PROPS.webIdePath);
expect(findWebIdeButton().text()).toBe('Web IDE');
expect(findWebIdeButton()).not.toBeDisabled();
});
@@ -72,13 +71,14 @@ describe('BlobEdit component', () => {
it('renders WebIdeLink component', () => {
createComponent(true);
- const { editPath: editUrl, webIdePath: webIdeUrl } = DEFAULT_PROPS;
+ const { editPath: editUrl, webIdePath: webIdeUrl, needsToFork } = DEFAULT_PROPS;
expect(findWebIdeLink().props()).toMatchObject({
editUrl,
webIdeUrl,
isBlob: true,
showEditButton: true,
+ needsToFork,
});
});
diff --git a/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js
new file mode 100644
index 00000000000..34448c03b31
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/video_viewer_spec.js
@@ -0,0 +1,22 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import VideoViewer from '~/repository/components/blob_viewers/video_viewer.vue';
+
+describe('Video Viewer', () => {
+ let wrapper;
+
+ const propsData = { url: 'some/video.mp4' };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(VideoViewer, { propsData });
+ };
+
+ const findVideo = () => wrapper.findByTestId('video');
+
+ it('renders a Video element', () => {
+ createComponent();
+
+ expect(findVideo().exists()).toBe(true);
+ expect(findVideo().attributes('src')).toBe(propsData.url);
+ expect(findVideo().attributes('controls')).not.toBeUndefined();
+ });
+});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index 0733cffe4f4..eb957c635ac 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -2,6 +2,7 @@ import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
+import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
const defaultMockRoute = {
name: 'blobPath',
@@ -10,7 +11,7 @@ const defaultMockRoute = {
describe('Repository breadcrumbs component', () => {
let wrapper;
- const factory = (currentPath, extraProps = {}, mockRoute = {}) => {
+ const factory = (currentPath, extraProps = {}, mockRoute = {}, newDirModal = true) => {
const $apollo = {
queries: {
userPermissions: {
@@ -34,10 +35,12 @@ describe('Repository breadcrumbs component', () => {
},
$apollo,
},
+ provide: { glFeatures: { newDirModal } },
});
};
const findUploadBlobModal = () => wrapper.find(UploadBlobModal);
+ const findNewDirectoryModal = () => wrapper.find(NewDirectoryModal);
afterEach(() => {
wrapper.destroy();
@@ -121,4 +124,37 @@ describe('Repository breadcrumbs component', () => {
expect(findUploadBlobModal().exists()).toBe(true);
});
});
+
+ describe('renders the new directory modal', () => {
+ describe('with the feature flag enabled', () => {
+ beforeEach(() => {
+ window.gon.features = {
+ newDirModal: true,
+ };
+ factory('/', { canEditTree: true });
+ });
+
+ it('does not render the modal while loading', () => {
+ expect(findNewDirectoryModal().exists()).toBe(false);
+ });
+
+ it('renders the modal once loaded', async () => {
+ wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findNewDirectoryModal().exists()).toBe(true);
+ });
+ });
+
+ describe('with the feature flag disabled', () => {
+ it('does not render the modal', () => {
+ window.gon.features = {
+ newDirModal: false,
+ };
+ factory('/', { canEditTree: true }, {}, {}, false);
+ expect(findNewDirectoryModal().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/repository/components/fork_suggestion_spec.js b/spec/frontend/repository/components/fork_suggestion_spec.js
new file mode 100644
index 00000000000..36a48a3fdb8
--- /dev/null
+++ b/spec/frontend/repository/components/fork_suggestion_spec.js
@@ -0,0 +1,44 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
+
+const DEFAULT_PROPS = { forkPath: 'some_file.js/fork' };
+
+describe('ForkSuggestion component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ForkSuggestion, {
+ propsData: { ...DEFAULT_PROPS },
+ });
+ };
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ const { i18n } = ForkSuggestion;
+ const findMessage = () => wrapper.findByTestId('message');
+ const findForkButton = () => wrapper.findByTestId('fork');
+ const findCancelButton = () => wrapper.findByTestId('cancel');
+
+ it('renders a message', () => {
+ expect(findMessage().text()).toBe(i18n.message);
+ });
+
+ it('renders a Fork button', () => {
+ const forkButton = findForkButton();
+
+ expect(forkButton.text()).toBe(i18n.fork);
+ expect(forkButton.attributes('href')).toBe(DEFAULT_PROPS.forkPath);
+ });
+
+ it('renders a Cancel button', () => {
+ expect(findCancelButton().text()).toBe(i18n.cancel);
+ });
+
+ it('emits a cancel event when Cancel button is clicked', () => {
+ findCancelButton().vm.$emit('click');
+
+ expect(wrapper.emitted('cancel')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
new file mode 100644
index 00000000000..fe7f024e3ea
--- /dev/null
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -0,0 +1,203 @@
+import { GlModal, GlFormTextarea, GlToggle } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { visitUrl } from '~/lib/utils/url_utility';
+import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
+
+const initialProps = {
+ modalTitle: 'Create New Directory',
+ modalId: 'modal-new-directory',
+ commitMessage: 'Add new directory',
+ targetBranch: 'some-target-branch',
+ originalBranch: 'master',
+ canPushCode: true,
+ path: 'create_dir',
+};
+
+const defaultFormValue = {
+ dirName: 'foo',
+ originalBranch: initialProps.originalBranch,
+ branchName: initialProps.targetBranch,
+ commitMessage: initialProps.commitMessage,
+ createNewMr: true,
+};
+
+describe('NewDirectoryModal', () => {
+ let wrapper;
+ let mock;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(NewDirectoryModal, {
+ propsData: {
+ ...initialProps,
+ ...props,
+ },
+ attrs: {
+ static: true,
+ visible: true,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findDirName = () => wrapper.find('[name="dir_name"]');
+ const findBranchName = () => wrapper.find('[name="branch_name"]');
+ const findCommitMessage = () => wrapper.findComponent(GlFormTextarea);
+ const findMrToggle = () => wrapper.findComponent(GlToggle);
+
+ const fillForm = async (inputValue = {}) => {
+ const {
+ dirName = defaultFormValue.dirName,
+ branchName = defaultFormValue.branchName,
+ commitMessage = defaultFormValue.commitMessage,
+ createNewMr = true,
+ } = inputValue;
+
+ await findDirName().vm.$emit('input', dirName);
+ await findBranchName().vm.$emit('input', branchName);
+ await findCommitMessage().vm.$emit('input', commitMessage);
+ await findMrToggle().vm.$emit('change', createNewMr);
+ await nextTick;
+ };
+
+ const submitForm = async () => {
+ const mockEvent = { preventDefault: jest.fn() };
+ findModal().vm.$emit('primary', mockEvent);
+ await waitForPromises();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders modal component', () => {
+ createComponent();
+
+ const { modalTitle: title } = initialProps;
+
+ expect(findModal().props()).toMatchObject({
+ title,
+ size: 'md',
+ actionPrimary: {
+ text: NewDirectoryModal.i18n.PRIMARY_OPTIONS_TEXT,
+ },
+ actionCancel: {
+ text: 'Cancel',
+ },
+ });
+ });
+
+ describe('form', () => {
+ it.each`
+ component | defaultValue | canPushCode | targetBranch | originalBranch | exist
+ ${findDirName} | ${undefined} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${findBranchName} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${findBranchName} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false}
+ ${findCommitMessage} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
+ ${findMrToggle} | ${'true'} | ${true} | ${'new-target-branch'} | ${'master'} | ${true}
+ ${findMrToggle} | ${'true'} | ${true} | ${'master'} | ${'master'} | ${true}
+ `(
+ 'has the correct form fields ',
+ ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
+ createComponent({
+ canPushCode,
+ targetBranch,
+ originalBranch,
+ });
+ const formField = component();
+
+ if (!exist) {
+ expect(formField.exists()).toBe(false);
+ return;
+ }
+
+ expect(formField.exists()).toBe(true);
+ expect(formField.attributes('value')).toBe(defaultValue);
+ },
+ );
+ });
+
+ describe('form submission', () => {
+ beforeEach(async () => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('valid form', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes the formData', async () => {
+ const {
+ dirName,
+ branchName,
+ commitMessage,
+ originalBranch,
+ createNewMr,
+ } = defaultFormValue;
+ mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {});
+ await fillForm();
+ await submitForm();
+
+ expect(mock.history.post[0].data.get('dir_name')).toEqual(dirName);
+ expect(mock.history.post[0].data.get('branch_name')).toEqual(branchName);
+ expect(mock.history.post[0].data.get('commit_message')).toEqual(commitMessage);
+ expect(mock.history.post[0].data.get('original_branch')).toEqual(originalBranch);
+ expect(mock.history.post[0].data.get('create_merge_request')).toEqual(String(createNewMr));
+ });
+
+ it('does not submit "create_merge_request" formData if createNewMr is not checked', async () => {
+ mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {});
+ await fillForm({ createNewMr: false });
+ await submitForm();
+ expect(mock.history.post[0].data.get('create_merge_request')).toBeNull();
+ });
+
+ it('redirects to the new directory', async () => {
+ const response = { filePath: 'new-dir-path' };
+ mock.onPost(initialProps.path).reply(httpStatusCodes.OK, response);
+
+ await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
+ await submitForm();
+
+ expect(visitUrl).toHaveBeenCalledWith(response.filePath);
+ });
+ });
+
+ describe('invalid form', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('disables submit button', async () => {
+ await fillForm({ dirName: '', branchName: '', commitMessage: '' });
+ expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true);
+ });
+
+ it('creates a flash error', async () => {
+ mock.onPost(initialProps.path).timeout();
+
+ await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
+ await submitForm();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: NewDirectoryModal.i18n.ERROR_MESSAGE,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 6f461f4c69b..26064e9b248 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -31,25 +31,36 @@ exports[`Repository table row component renders a symlink table row 1`] = `
<!---->
- <!---->
+ <gl-icon-stub
+ class="ml-1"
+ name="lock"
+ size="12"
+ title="Locked by Root"
+ />
</td>
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
>
- <gl-skeleton-loading-stub
- class="h-auto"
- lines="1"
+ <gl-link-stub
+ class="str-truncated-100 tree-commit-link"
/>
+
+ <gl-intersection-observer-stub>
+ <!---->
+ </gl-intersection-observer-stub>
</td>
<td
class="tree-time-ago text-right cursor-default"
>
- <gl-skeleton-loading-stub
- class="ml-auto h-auto w-50"
- lines="1"
+ <timeago-tooltip-stub
+ cssclass=""
+ time="2019-01-01"
+ tooltipplacement="top"
/>
+
+ <!---->
</td>
</tr>
`;
@@ -85,25 +96,36 @@ exports[`Repository table row component renders table row 1`] = `
<!---->
- <!---->
+ <gl-icon-stub
+ class="ml-1"
+ name="lock"
+ size="12"
+ title="Locked by Root"
+ />
</td>
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
>
- <gl-skeleton-loading-stub
- class="h-auto"
- lines="1"
+ <gl-link-stub
+ class="str-truncated-100 tree-commit-link"
/>
+
+ <gl-intersection-observer-stub>
+ <!---->
+ </gl-intersection-observer-stub>
</td>
<td
class="tree-time-ago text-right cursor-default"
>
- <gl-skeleton-loading-stub
- class="ml-auto h-auto w-50"
- lines="1"
+ <timeago-tooltip-stub
+ cssclass=""
+ time="2019-01-01"
+ tooltipplacement="top"
/>
+
+ <!---->
</td>
</tr>
`;
@@ -139,25 +161,36 @@ exports[`Repository table row component renders table row for path with special
<!---->
- <!---->
+ <gl-icon-stub
+ class="ml-1"
+ name="lock"
+ size="12"
+ title="Locked by Root"
+ />
</td>
<td
class="d-none d-sm-table-cell tree-commit cursor-default"
>
- <gl-skeleton-loading-stub
- class="h-auto"
- lines="1"
+ <gl-link-stub
+ class="str-truncated-100 tree-commit-link"
/>
+
+ <gl-intersection-observer-stub>
+ <!---->
+ </gl-intersection-observer-stub>
</td>
<td
class="tree-time-ago text-right cursor-default"
>
- <gl-skeleton-loading-stub
- class="ml-auto h-auto w-50"
- lines="1"
+ <timeago-tooltip-stub
+ cssclass=""
+ time="2019-01-01"
+ tooltipplacement="top"
/>
+
+ <!---->
</td>
</tr>
`;
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index e9e51abaf0f..c8dddefc4f2 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -34,17 +34,45 @@ const MOCK_BLOBS = [
},
];
-function factory({ path, isLoading = false, hasMore = true, entries = {} }) {
+const MOCK_COMMITS = [
+ {
+ fileName: 'blob.md',
+ type: 'blob',
+ commit: {
+ message: 'Updated blob.md',
+ },
+ },
+ {
+ fileName: 'blob2.md',
+ type: 'blob',
+ commit: {
+ message: 'Updated blob2.md',
+ },
+ },
+ {
+ fileName: 'blob3.md',
+ type: 'blob',
+ commit: {
+ message: 'Updated blob3.md',
+ },
+ },
+];
+
+function factory({ path, isLoading = false, hasMore = true, entries = {}, commits = [] }) {
vm = shallowMount(Table, {
propsData: {
path,
isLoading,
entries,
hasMore,
+ commits,
},
mocks: {
$apollo,
},
+ provide: {
+ glFeatures: { lazyLoadCommits: true },
+ },
});
}
@@ -82,12 +110,15 @@ describe('Repository table component', () => {
entries: {
blobs: MOCK_BLOBS,
},
+ commits: MOCK_COMMITS,
});
const rows = vm.findAll(TableRow);
expect(rows.length).toEqual(3);
expect(rows.at(2).attributes().mode).toEqual('120000');
+ expect(rows.at(2).props().rowNumber).toBe(2);
+ expect(rows.at(2).props().commitInfo).toEqual(MOCK_COMMITS[2]);
});
describe('Show more button', () => {
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index da28c9873d9..76e9f7da011 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -1,10 +1,12 @@
-import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
+import { GlBadge, GlLink, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import TableRow from '~/repository/components/table/row.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
+const COMMIT_MOCK = { lockLabel: 'Locked by Root', committedDate: '2019-01-01' };
+
let vm;
let $router;
@@ -20,12 +22,14 @@ function factory(propsData = {}) {
projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
totalEntries: 10,
+ commitInfo: COMMIT_MOCK,
+ rowNumber: 123,
},
directives: {
GlHoverLoad: createMockDirective(),
},
provide: {
- glFeatures: { refactorBlobViewer: true },
+ glFeatures: { refactorBlobViewer: true, lazyLoadCommits: true },
},
mocks: {
$router,
@@ -40,6 +44,7 @@ function factory(propsData = {}) {
describe('Repository table row component', () => {
const findRouterLink = () => vm.find(RouterLinkStub);
+ const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver);
afterEach(() => {
vm.destroy();
@@ -226,8 +231,6 @@ describe('Repository table row component', () => {
currentPath: '/',
});
- vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } });
-
return vm.vm.$nextTick().then(() => {
expect(vm.find(GlIcon).exists()).toBe(true);
expect(vm.find(GlIcon).props('name')).toBe('lock');
@@ -246,4 +249,27 @@ describe('Repository table row component', () => {
expect(vm.find(FileIcon).props('loading')).toBe(true);
});
+
+ describe('row visibility', () => {
+ beforeEach(() => {
+ factory({
+ id: '1',
+ sha: '1',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ });
+ });
+ it('emits a `row-appear` event', () => {
+ findIntersectionObserver().vm.$emit('appear');
+ expect(vm.emitted('row-appear')).toEqual([
+ [
+ {
+ hasCommit: true,
+ rowNumber: 123,
+ },
+ ],
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index e36287eff29..49397c77215 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -3,6 +3,13 @@ import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.g
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from '~/repository/components/tree_content.vue';
+import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
+
+jest.mock('~/repository/commits_service', () => ({
+ loadCommits: jest.fn(() => Promise.resolve()),
+ isRequested: jest.fn(),
+ resetRequestedCommits: jest.fn(),
+}));
let vm;
let $apollo;
@@ -23,6 +30,7 @@ function factory(path, data = () => ({})) {
glFeatures: {
increasePageSizeExponentially: true,
paginatedTreeGraphqlQuery: true,
+ lazyLoadCommits: true,
},
},
});
@@ -45,7 +53,7 @@ describe('Repository table component', () => {
expect(vm.find(FilePreview).exists()).toBe(true);
});
- it('trigger fetchFiles when mounted', async () => {
+ it('trigger fetchFiles and resetRequestedCommits when mounted', async () => {
factory('/');
jest.spyOn(vm.vm, 'fetchFiles').mockImplementation(() => {});
@@ -53,6 +61,7 @@ describe('Repository table component', () => {
await vm.vm.$nextTick();
expect(vm.vm.fetchFiles).toHaveBeenCalled();
+ expect(resetRequestedCommits).toHaveBeenCalled();
});
describe('normalizeData', () => {
@@ -180,4 +189,15 @@ describe('Repository table component', () => {
});
});
});
+
+ it('loads commit data when row-appear event is emitted', () => {
+ const path = 'some/path';
+ const rowNumber = 1;
+
+ factory(path);
+ findFileTable().vm.$emit('row-appear', { hasCommit: false, rowNumber });
+
+ expect(isRequested).toHaveBeenCalledWith(rowNumber);
+ expect(loadCommits).toHaveBeenCalledWith('', path, '', rowNumber);
+ });
});
diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js
index bb82fa706fd..3f822db601f 100644
--- a/spec/frontend/repository/router_spec.js
+++ b/spec/frontend/repository/router_spec.js
@@ -24,4 +24,32 @@ describe('Repository router spec', () => {
expect(componentsForRoute).toContain(component);
}
});
+
+ describe('Storing Web IDE path globally', () => {
+ const proj = 'foo-bar-group/foo-bar-proj';
+ let originalGl;
+
+ beforeEach(() => {
+ originalGl = window.gl;
+ });
+
+ afterEach(() => {
+ window.gl = originalGl;
+ });
+
+ it.each`
+ path | branch | expectedPath
+ ${'/'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
+ ${'/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
+ ${'/tree/feat(test)'} | ${'feat(test)'} | ${`/-/ide/project/${proj}/edit/feat(test)/-/`}
+ ${'/-/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`}
+ ${'/-/tree/main/app/assets'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/app/assets/`}
+ ${'/-/blob/main/file.md'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/file.md`}
+ `('generates the correct Web IDE url for $path', ({ path, branch, expectedPath } = {}) => {
+ const router = createRouter(proj, branch);
+
+ router.push(path);
+ expect(window.gl.webIDEPath).toBe(expectedPath);
+ });
+ });
});
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 3292f635f6b..33e9c122080 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -1,3 +1,4 @@
+import { GlLink } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -5,6 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
@@ -12,7 +14,6 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
-import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
@@ -49,7 +50,6 @@ describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
- const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -86,10 +86,6 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
- it('shows the runner type help', () => {
- expect(findRunnerTypeHelp().exists()).toBe(true);
- });
-
it('shows the runner setup instructions', () => {
expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
});
@@ -98,6 +94,20 @@ describe('AdminRunnersApp', () => {
expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
});
+ it('runner item links to the runner admin page', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const { id, shortSha } = runnersData.data.runners.nodes[0];
+ const numericId = getIdFromGraphQLId(id);
+
+ const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
+
+ expect(runnerLink.text()).toBe(`#${numericId} (${shortSha})`);
+ expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${numericId}`);
+ });
+
it('requests the runners with no filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: undefined,
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 95f7c38cafc..5aa3879ac3e 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -5,15 +5,18 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
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 { captureException } from '~/runner/sentry_utils';
-import { runnerData } from '../../mock_data';
+import { runnersData, runnerData } from '../../mock_data';
-const mockRunner = runnerData.data.runner;
+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;
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -36,6 +39,7 @@ describe('RunnerTypeCell', () => {
propsData: {
runner: {
id: mockRunner.id,
+ adminUrl: mockRunner.adminUrl,
active,
},
},
@@ -61,7 +65,7 @@ describe('RunnerTypeCell', () => {
runnerUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
- runner: runnerData.data.runner,
+ runner: mockRunnerDetails,
errors: [],
},
},
@@ -78,7 +82,7 @@ describe('RunnerTypeCell', () => {
it('Displays the runner edit link with the correct href', () => {
createComponent();
- expect(findEditBtn().attributes('href')).toBe('/admin/runners/1');
+ expect(findEditBtn().attributes('href')).toBe(mockRunner.adminUrl);
});
describe.each`
@@ -231,7 +235,7 @@ describe('RunnerTypeCell', () => {
},
},
awaitRefetchQueries: true,
- refetchQueries: [getRunnersQueryName],
+ refetchQueries: [getRunnersQueryName, getGroupRunnersQueryName],
});
});
diff --git a/spec/frontend/runner/components/cells/runner_name_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
index 26055fc0faf..1c9282e0acd 100644
--- a/spec/frontend/runner/components/cells/runner_name_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
@@ -1,6 +1,5 @@
-import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import RunnerNameCell from '~/runner/components/cells/runner_name_cell.vue';
+import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
const mockId = '1';
const mockShortSha = '2P6oDVDm';
@@ -9,10 +8,8 @@ const mockDescription = 'runner-1';
describe('RunnerTypeCell', () => {
let wrapper;
- const findLink = () => wrapper.findComponent(GlLink);
-
- const createComponent = () => {
- wrapper = mount(RunnerNameCell, {
+ const createComponent = (options) => {
+ wrapper = mount(RunnerSummaryCell, {
propsData: {
runner: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
@@ -20,6 +17,7 @@ describe('RunnerTypeCell', () => {
description: mockDescription,
},
},
+ ...options,
});
};
@@ -31,12 +29,23 @@ describe('RunnerTypeCell', () => {
wrapper.destroy();
});
- it('Displays the runner link with id and short token', () => {
- expect(findLink().text()).toBe(`#${mockId} (${mockShortSha})`);
- expect(findLink().attributes('href')).toBe(`/admin/runners/${mockId}`);
+ it('Displays the runner name as id and short token', () => {
+ expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`);
});
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockDescription);
});
+
+ it('Displays a custom slot', () => {
+ const slotContent = 'My custom runner summary';
+
+ createComponent({
+ slots: {
+ 'runner-name': slotContent,
+ },
+ });
+
+ expect(wrapper.text()).toContain(slotContent);
+ });
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 344d1e5c150..e24dffea1eb 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlTable, GlSkeletonLoader } from '@gitlab/ui';
+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';
@@ -67,11 +67,11 @@ describe('RunnerList', () => {
// Badges
expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused');
- // Runner identifier
- expect(findCell({ fieldKey: 'name' }).text()).toContain(
+ // Runner summary
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(
`#${getIdFromGraphQLId(id)} (${shortSha})`,
);
- expect(findCell({ fieldKey: 'name' }).text()).toContain(description);
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(description);
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
@@ -136,12 +136,11 @@ describe('RunnerList', () => {
});
});
- it('Links to the runner page', () => {
- const { id } = mockRunners[0];
+ it('Shows runner identifier', () => {
+ const { id, shortSha } = mockRunners[0];
+ const numericId = getIdFromGraphQLId(id);
- expect(findCell({ fieldKey: 'name' }).find(GlLink).attributes('href')).toBe(
- `/admin/runners/${getIdFromGraphQLId(id)}`,
- );
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(`#${numericId} (${shortSha})`);
});
describe('When data is loading', () => {
diff --git a/spec/frontend/runner/components/runner_state_locked_badge_spec.js b/spec/frontend/runner/components/runner_state_locked_badge_spec.js
new file mode 100644
index 00000000000..e92b671f5a1
--- /dev/null
+++ b/spec/frontend/runner/components/runner_state_locked_badge_spec.js
@@ -0,0 +1,45 @@
+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_state_paused_badge_spec.js b/spec/frontend/runner/components/runner_state_paused_badge_spec.js
new file mode 100644
index 00000000000..8df56d6e3f3
--- /dev/null
+++ b/spec/frontend/runner/components/runner_state_paused_badge_spec.js
@@ -0,0 +1,45 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerStatePausedBadge from '~/runner/components/runner_state_paused_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(RunnerStatePausedBadge, {
+ propsData: {
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders paused state', () => {
+ expect(wrapper.text()).toBe('paused');
+ expect(findBadge().props('variant')).toBe('danger');
+ });
+
+ 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_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js
index ab5ccf6390f..fb344e65389 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/runner/components/runner_type_badge_spec.js
@@ -1,18 +1,23 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
const findBadge = () => wrapper.findComponent(GlBadge);
+ const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTypeBadge, {
propsData: {
...props,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
};
@@ -20,16 +25,24 @@ describe('RunnerTypeBadge', () => {
wrapper.destroy();
});
- it.each`
+ describe.each`
type | text | variant
${INSTANCE_TYPE} | ${'shared'} | ${'success'}
${GROUP_TYPE} | ${'group'} | ${'success'}
${PROJECT_TYPE} | ${'specific'} | ${'info'}
- `('displays $type runner with as "$text" with a $variant variant ', ({ type, text, variant }) => {
- createComponent({ props: { type } });
+ `('displays $type runner', ({ type, text, variant }) => {
+ beforeEach(() => {
+ createComponent({ props: { type } });
+ });
- expect(findBadge().text()).toBe(text);
- expect(findBadge().props('variant')).toBe(variant);
+ it(`as "${text}" with a ${variant} variant`, () => {
+ expect(findBadge().text()).toBe(text);
+ expect(findBadge().props('variant')).toBe(variant);
+ });
+
+ it('with a tooltip', () => {
+ expect(getTooltip().value).toBeDefined();
+ });
});
it('validation fails for an incorrect type', () => {
diff --git a/spec/frontend/runner/components/runner_type_help_spec.js b/spec/frontend/runner/components/runner_type_help_spec.js
deleted file mode 100644
index f0d03282f8e..00000000000
--- a/spec/frontend/runner/components/runner_type_help_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
-
-describe('RunnerTypeHelp', () => {
- let wrapper;
-
- const findBadges = () => wrapper.findAllComponents(GlBadge);
-
- const createComponent = () => {
- wrapper = mount(RunnerTypeHelp);
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays each of the runner types', () => {
- expect(findBadges().at(0).text()).toBe('shared');
- expect(findBadges().at(1).text()).toBe('group');
- expect(findBadges().at(2).text()).toBe('specific');
- });
-
- it('Displays runner states', () => {
- expect(findBadges().at(3).text()).toBe('locked');
- expect(findBadges().at(4).text()).toBe('paused');
- });
-});
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 e80da40e3bd..5f3aabd4bc3 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 { GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -5,13 +6,13 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
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 RunnerPagination from '~/runner/components/runner_pagination.vue';
-import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
import {
CREATED_ASC,
@@ -34,8 +35,7 @@ localVue.use(VueApollo);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
-const mockRunners = groupRunnersData.data.group.runners.nodes;
-const mockGroupRunnersLimitedCount = mockRunners.length;
+const mockGroupRunnersLimitedCount = groupRunnersData.data.group.runners.edges.length;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -48,7 +48,6 @@ describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
- const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -82,16 +81,27 @@ describe('GroupRunnersApp', () => {
await waitForPromises();
});
- it('shows the runner type help', () => {
- expect(findRunnerTypeHelp().exists()).toBe(true);
- });
-
it('shows the runner setup instructions', () => {
expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
});
it('shows the runners list', () => {
- expect(findRunnerList().props('runners')).toEqual(groupRunnersData.data.group.runners.nodes);
+ expect(findRunnerList().props('runners')).toEqual(
+ groupRunnersData.data.group.runners.edges.map(({ node }) => node),
+ );
+ });
+
+ it('runner item links to the runner group page', async () => {
+ const { webUrl, node } = groupRunnersData.data.group.runners.edges[0];
+ const { id, shortSha } = node;
+
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
+ expect(runnerLink.text()).toBe(`#${getIdFromGraphQLId(id)} (${shortSha})`);
+ expect(runnerLink.attributes('href')).toBe(webUrl);
});
it('requests the runners with group path and no other filters', () => {
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index c90b9a4c426..b8d0f1273c7 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -1,14 +1,18 @@
-const runnerFixture = (filename) => getJSONFixture(`graphql/runner/${filename}`);
-
// Fixtures generated by: spec/frontend/fixtures/runner.rb
// Admin queries
-export const runnersData = runnerFixture('get_runners.query.graphql.json');
-export const runnersDataPaginated = runnerFixture('get_runners.query.graphql.paginated.json');
-export const runnerData = runnerFixture('get_runner.query.graphql.json');
+import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.json';
+import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json';
+import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
// Group queries
-export const groupRunnersData = runnerFixture('get_group_runners.query.graphql.json');
-export const groupRunnersDataPaginated = runnerFixture(
- 'get_group_runners.query.graphql.paginated.json',
-);
+import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
+import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json';
+
+export {
+ runnerData,
+ runnersDataPaginated,
+ runnersData,
+ groupRunnersData,
+ groupRunnersDataPaginated,
+};
diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js
index 173936e1ce3..6beaea8dba5 100644
--- a/spec/frontend/search_settings/components/search_settings_spec.js
+++ b/spec/frontend/search_settings/components/search_settings_spec.js
@@ -11,6 +11,7 @@ describe('search_settings/components/search_settings.vue', () => {
const GENERAL_SETTINGS_ID = 'js-general-settings';
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
const EXTRA_SETTINGS_ID = 'js-extra-settings';
+ const TEXT_CONTAIN_SEARCH_TERM = `This text contain ${SEARCH_TERM} and <script>alert("111")</script> others.`;
let wrapper;
@@ -33,6 +34,21 @@ describe('search_settings/components/search_settings.vue', () => {
const visibleSectionsCount = () =>
document.querySelectorAll(`${SECTION_SELECTOR}:not(.${HIDE_CLASS})`).length;
const highlightedElementsCount = () => document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).length;
+
+ const highlightedTextNodes = () => {
+ const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
+ return highlightedList.every((element) => {
+ return element.textContent.toLowerCase() === SEARCH_TERM.toLowerCase();
+ });
+ };
+
+ const matchParentElement = () => {
+ const highlightedList = Array.from(document.querySelectorAll(`.${HIGHLIGHT_CLASS}`));
+ return highlightedList.map((element) => {
+ return element.parentNode;
+ });
+ };
+
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const search = (term) => {
findSearchBox().vm.$emit('input', term);
@@ -52,6 +68,7 @@ describe('search_settings/components/search_settings.vue', () => {
</section>
<section id="${EXTRA_SETTINGS_ID}" class="settings">
<span>${SEARCH_TERM}</span>
+ <span>${TEXT_CONTAIN_SEARCH_TERM}</span>
</section>
</div>
</div>
@@ -82,7 +99,23 @@ describe('search_settings/components/search_settings.vue', () => {
it('highlight elements that match the search term', () => {
search(SEARCH_TERM);
- expect(highlightedElementsCount()).toBe(1);
+ expect(highlightedElementsCount()).toBe(2);
+ });
+
+ it('highlight only search term and not the whole line', () => {
+ search(SEARCH_TERM);
+
+ expect(highlightedTextNodes()).toBe(true);
+ });
+
+ it('prevents search xss', () => {
+ search(SEARCH_TERM);
+
+ const parentNodeList = matchParentElement();
+ parentNodeList.forEach((element) => {
+ const scriptElement = element.getElementsByTagName('script');
+ expect(scriptElement.length).toBe(0);
+ });
});
describe('default', () => {
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index be27a800418..b3a67f18f82 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import UsersMock from './mock_data';
describe('Assignee component', () => {
@@ -19,6 +20,7 @@ describe('Assignee component', () => {
});
};
+ const findAllAvatarLinks = () => wrapper.findAllComponents(AssigneeAvatarLink);
const findComponentTextNoUsers = () => wrapper.find('[data-testid="no-value"]');
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
@@ -148,7 +150,7 @@ describe('Assignee component', () => {
editable: true,
});
- expect(wrapper.findAll('.user-item').length).toBe(users.length);
+ expect(findAllAvatarLinks()).toHaveLength(users.length);
expect(wrapper.find('.user-list-more').exists()).toBe(false);
});
@@ -178,9 +180,9 @@ describe('Assignee component', () => {
users,
});
- const userItems = wrapper.findAll('.user-list .user-item a');
+ const userItems = findAllAvatarLinks();
- expect(userItems.length).toBe(3);
+ expect(userItems).toHaveLength(3);
expect(userItems.at(0).attributes('title')).toBe(users[2].name);
});
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 9f6878db785..6b80224083a 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -26,9 +26,9 @@ describe('UncollapsedReviewerList component', () => {
});
describe('single reviewer', () => {
- beforeEach(() => {
- const user = userDataMock();
+ const user = userDataMock();
+ beforeEach(() => {
createComponent({
users: [user],
});
@@ -39,6 +39,7 @@ describe('UncollapsedReviewerList component', () => {
});
it('shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain(`@root`);
});
@@ -56,11 +57,18 @@ describe('UncollapsedReviewerList component', () => {
});
describe('multiple reviewers', () => {
- beforeEach(() => {
- const user = userDataMock();
+ const user = userDataMock();
+ const user2 = {
+ ...user,
+ id: 2,
+ name: 'nonrooty-nonrootersen',
+ username: 'hello-world',
+ approved: true,
+ };
+ beforeEach(() => {
createComponent({
- users: [user, { ...user, id: 2, username: 'hello-world', approved: true }],
+ users: [user, user2],
});
});
@@ -69,7 +77,9 @@ describe('UncollapsedReviewerList component', () => {
});
it('shows both users with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain(`@root`);
+ expect(wrapper.text()).toContain(user2.name);
expect(wrapper.text()).toContain(`@hello-world`);
});
diff --git a/spec/frontend/sidebar/sidebar_labels_spec.js b/spec/frontend/sidebar/sidebar_labels_spec.js
index 7455f684380..8437ee1b723 100644
--- a/spec/frontend/sidebar/sidebar_labels_spec.js
+++ b/spec/frontend/sidebar/sidebar_labels_spec.js
@@ -27,6 +27,7 @@ describe('sidebar labels', () => {
labelsManagePath: '/gitlab-org/gitlab-test/-/labels',
projectIssuesPath: '/gitlab-org/gitlab-test/-/issues',
projectPath: 'gitlab-org/gitlab-test',
+ fullPath: 'gitlab-org/gitlab-test',
};
const $apollo = {
@@ -110,10 +111,9 @@ describe('sidebar labels', () => {
mutation: updateIssueLabelsMutation,
variables: {
input: {
- addLabelIds: [40],
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
- removeLabelIds: [26, 55],
+ labelIds: [toLabelGid(29), toLabelGid(28), toLabelGid(27), toLabelGid(40)],
},
},
};
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index ff6da3abad0..6829e688c65 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -27,7 +27,7 @@ describe('SidebarTodo', () => {
it.each`
state | classes
${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
- ${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'dont-change-state']}
+ ${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'js-dont-change-state']}
`('returns todo button classes for when `collapsed` prop is `$state`', ({ state, classes }) => {
createComponent({ collapsed: state });
expect(wrapper.find('button').classes()).toStrictEqual(classes);
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b7b638b5137..af61f4ea54f 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -41,19 +41,23 @@ describe('Snippet view app', () => {
},
});
}
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmbedDropdown = () => wrapper.findComponent(EmbedDropdown);
+
afterEach(() => {
wrapper.destroy();
});
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('renders all simple components after the query is finished', () => {
+ it('renders all simple components required after the query is finished', () => {
createComponent();
- expect(wrapper.find(SnippetHeader).exists()).toBe(true);
- expect(wrapper.find(SnippetTitle).exists()).toBe(true);
+ expect(wrapper.findComponent(SnippetHeader).exists()).toBe(true);
+ expect(wrapper.findComponent(SnippetTitle).exists()).toBe(true);
});
it('renders embed dropdown component if visibility allows', () => {
@@ -65,7 +69,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.find(EmbedDropdown).exists()).toBe(true);
+ expect(findEmbedDropdown().exists()).toBe(true);
});
it('renders correct snippet-blob components', () => {
@@ -98,7 +102,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.find(EmbedDropdown).exists()).toBe(isRendered);
+ expect(findEmbedDropdown().exists()).toBe(isRendered);
});
});
@@ -120,7 +124,7 @@ describe('Snippet view app', () => {
},
},
});
- expect(wrapper.find(CloneDropdownButton).exists()).toBe(isRendered);
+ expect(wrapper.findComponent(CloneDropdownButton).exists()).toBe(isRendered);
},
);
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index fb95be3a77c..552a1c6fcde 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,23 +1,30 @@
import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
+import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
-import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
+import axios from '~/lib/utils/axios_utils';
+import createFlash, { FLASH_TYPES } from '~/flash';
+
+jest.mock('~/flash');
describe('Snippet header component', () => {
let wrapper;
let snippet;
let mutationTypes;
let mutationVariables;
+ let mock;
let errorMsg;
let err;
const originalRelativeUrlRoot = gon.relative_url_root;
const reportAbusePath = '/-/snippets/42/mark_as_spam';
+ const canReportSpam = true;
const GlEmoji = { template: '<img/>' };
@@ -47,6 +54,7 @@ describe('Snippet header component', () => {
mocks: { $apollo },
provide: {
reportAbusePath,
+ canReportSpam,
...provide,
},
propsData: {
@@ -118,10 +126,13 @@ describe('Snippet header component', () => {
RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
REJECT: jest.fn(() => Promise.reject(err)),
};
+
+ mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
+ mock.restore();
gon.relative_url_root = originalRelativeUrlRoot;
});
@@ -186,7 +197,6 @@ describe('Snippet header component', () => {
{
category: 'primary',
disabled: false,
- href: reportAbusePath,
text: 'Submit as spam',
variant: 'default',
},
@@ -205,7 +215,6 @@ describe('Snippet header component', () => {
text: 'Delete',
},
{
- href: reportAbusePath,
text: 'Submit as spam',
title: 'Submit as spam',
},
@@ -249,6 +258,31 @@ describe('Snippet header component', () => {
);
});
+ describe('submit snippet as spam', () => {
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ it.each`
+ request | variant | text
+ ${200} | ${'SUCCESS'} | ${i18n.snippetSpamSuccess}
+ ${500} | ${'DANGER'} | ${i18n.snippetSpamFailure}
+ `(
+ 'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
+ async ({ request, variant, text }) => {
+ const submitAsSpamBtn = findButtons().at(2);
+ mock.onPost(reportAbusePath).reply(request);
+ submitAsSpamBtn.trigger('click');
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenLastCalledWith({
+ message: expect.stringContaining(text),
+ type: FLASH_TYPES[variant],
+ });
+ },
+ );
+ });
+
describe('with guest user', () => {
beforeEach(() => {
createComponent({
@@ -258,6 +292,7 @@ describe('Snippet header component', () => {
},
provide: {
reportAbusePath: null,
+ canReportSpam: false,
},
});
});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 4d1b0f54e42..2c8e0fff848 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -6,7 +6,7 @@ import { setGlobalDateToFakeDate } from 'helpers/fake_date';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import Translate from '~/vue_shared/translate';
-import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
+import { loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
import { initializeTestTimeout } from './__helpers__/timeout';
import customMatchers from './matchers';
import { setupManualMocks } from './mocks/mocks_helper';
@@ -43,7 +43,6 @@ Vue.use(Translate);
// convenience wrapper for migration from Karma
Object.assign(global, {
- getJSONFixture,
loadFixtures: loadHTMLFixture,
setFixtures: setHTMLFixture,
});
diff --git a/spec/frontend/tracking/get_standard_context_spec.js b/spec/frontend/tracking/get_standard_context_spec.js
index b7bdc56b801..ada914b586c 100644
--- a/spec/frontend/tracking/get_standard_context_spec.js
+++ b/spec/frontend/tracking/get_standard_context_spec.js
@@ -1,5 +1,13 @@
-import { SNOWPLOW_JS_SOURCE } from '~/tracking/constants';
+import { SNOWPLOW_JS_SOURCE, GOOGLE_ANALYTICS_ID_COOKIE_NAME } from '~/tracking/constants';
import getStandardContext from '~/tracking/get_standard_context';
+import { setCookie, removeCookie } from '~/lib/utils/common_utils';
+
+const TEST_GA_ID = 'GA1.2.345678901.234567891';
+const TEST_BASE_DATA = {
+ source: SNOWPLOW_JS_SOURCE,
+ google_analytics_id: '',
+ extra: {},
+};
describe('~/tracking/get_standard_context', () => {
beforeEach(() => {
@@ -10,10 +18,7 @@ describe('~/tracking/get_standard_context', () => {
it('returns default object if called without server context', () => {
expect(getStandardContext()).toStrictEqual({
schema: undefined,
- data: {
- source: SNOWPLOW_JS_SOURCE,
- extra: {},
- },
+ data: TEST_BASE_DATA,
});
});
@@ -28,9 +33,8 @@ describe('~/tracking/get_standard_context', () => {
expect(getStandardContext()).toStrictEqual({
schema: 'iglu:com.gitlab/gitlab_standard',
data: {
+ ...TEST_BASE_DATA,
environment: 'testing',
- source: SNOWPLOW_JS_SOURCE,
- extra: {},
},
});
});
@@ -50,4 +54,15 @@ describe('~/tracking/get_standard_context', () => {
expect(getStandardContext({ extra }).data.extra).toBe(extra);
});
+
+ describe('with Google Analytics cookie present', () => {
+ afterEach(() => {
+ removeCookie(GOOGLE_ANALYTICS_ID_COOKIE_NAME);
+ });
+
+ it('appends Google Analytics ID', () => {
+ setCookie(GOOGLE_ANALYTICS_ID_COOKIE_NAME, TEST_GA_ID);
+ expect(getStandardContext().data.google_analytics_id).toBe(TEST_GA_ID);
+ });
+ });
});
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
new file mode 100644
index 00000000000..2b70aacc4cb
--- /dev/null
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -0,0 +1,140 @@
+import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
+import { getExperimentData, getAllExperimentContexts } from '~/experimentation/utils';
+import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
+import getStandardContext from '~/tracking/get_standard_context';
+
+jest.mock('~/experimentation/utils', () => ({
+ getExperimentData: jest.fn(),
+ getAllExperimentContexts: jest.fn(),
+}));
+
+describe('Tracking', () => {
+ let standardContext;
+ let snowplowSpy;
+ let bindDocumentSpy;
+ let trackLoadEventsSpy;
+ let enableFormTracking;
+ let setAnonymousUrlsSpy;
+
+ beforeAll(() => {
+ window.gl = window.gl || {};
+ window.gl.snowplowStandardContext = {
+ schema: 'iglu:com.gitlab/gitlab_standard',
+ data: {
+ environment: 'testing',
+ source: 'unknown',
+ extra: {},
+ },
+ };
+
+ standardContext = getStandardContext();
+ });
+
+ beforeEach(() => {
+ getExperimentData.mockReturnValue(undefined);
+ getAllExperimentContexts.mockReturnValue([]);
+
+ window.snowplow = window.snowplow || (() => {});
+ window.snowplowOptions = {
+ namespace: 'gl_test',
+ hostname: 'app.test.com',
+ cookieDomain: '.test.com',
+ };
+
+ snowplowSpy = jest.spyOn(window, 'snowplow');
+ });
+
+ describe('initUserTracking', () => {
+ it('calls through to get a new tracker with the expected options', () => {
+ initUserTracking();
+ expect(snowplowSpy).toHaveBeenCalledWith('newTracker', 'gl_test', 'app.test.com', {
+ namespace: 'gl_test',
+ hostname: 'app.test.com',
+ cookieDomain: '.test.com',
+ appId: '',
+ userFingerprint: false,
+ respectDoNotTrack: true,
+ forceSecureTracker: true,
+ eventMethod: 'post',
+ contexts: { webPage: true, performanceTiming: true },
+ formTracking: false,
+ linkClickTracking: false,
+ pageUnloadTimer: 10,
+ formTrackingConfig: {
+ fields: { allow: [] },
+ forms: { allow: [] },
+ },
+ });
+ });
+ });
+
+ describe('initDefaultTrackers', () => {
+ beforeEach(() => {
+ bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
+ trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
+ enableFormTracking = jest
+ .spyOn(Tracking, 'enableFormTracking')
+ .mockImplementation(() => null);
+ setAnonymousUrlsSpy = jest.spyOn(Tracking, 'setAnonymousUrls').mockImplementation(() => null);
+ });
+
+ it('should activate features based on what has been enabled', () => {
+ initDefaultTrackers();
+ expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [standardContext]);
+ expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
+ expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
+
+ window.snowplowOptions = {
+ ...window.snowplowOptions,
+ formTracking: true,
+ linkClickTracking: true,
+ formTrackingConfig: { forms: { whitelist: ['foo'] }, fields: { whitelist: ['bar'] } },
+ };
+
+ initDefaultTrackers();
+ expect(enableFormTracking).toHaveBeenCalledWith(window.snowplowOptions.formTrackingConfig);
+ expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking');
+ });
+
+ it('binds the document event handling', () => {
+ initDefaultTrackers();
+ expect(bindDocumentSpy).toHaveBeenCalled();
+ });
+
+ it('tracks page loaded events', () => {
+ initDefaultTrackers();
+ expect(trackLoadEventsSpy).toHaveBeenCalled();
+ });
+
+ it('calls the anonymized URLs method', () => {
+ initDefaultTrackers();
+ expect(setAnonymousUrlsSpy).toHaveBeenCalled();
+ });
+
+ describe('when there are experiment contexts', () => {
+ const experimentContexts = [
+ {
+ schema: TRACKING_CONTEXT_SCHEMA,
+ data: { experiment: 'experiment1', variant: 'control' },
+ },
+ {
+ schema: TRACKING_CONTEXT_SCHEMA,
+ data: { experiment: 'experiment_two', variant: 'candidate' },
+ },
+ ];
+
+ beforeEach(() => {
+ getAllExperimentContexts.mockReturnValue(experimentContexts);
+ });
+
+ it('includes those contexts alongside the standard context', () => {
+ initDefaultTrackers();
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [
+ standardContext,
+ ...experimentContexts,
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index 21fed51ff10..b7a2e4f4f51 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -8,16 +8,16 @@ import getStandardContext from '~/tracking/get_standard_context';
jest.mock('~/experimentation/utils', () => ({
getExperimentData: jest.fn(),
- getAllExperimentContexts: jest.fn(),
+ getAllExperimentContexts: jest.fn().mockReturnValue([]),
}));
+const TEST_CATEGORY = 'root:index';
+const TEST_ACTION = 'generic';
+const TEST_LABEL = 'button';
+
describe('Tracking', () => {
let standardContext;
let snowplowSpy;
- let bindDocumentSpy;
- let trackLoadEventsSpy;
- let enableFormTracking;
- let setAnonymousUrlsSpy;
beforeAll(() => {
window.gl = window.gl || {};
@@ -30,132 +30,46 @@ describe('Tracking', () => {
extra: {},
},
};
+ window.snowplowOptions = {
+ namespace: 'gl_test',
+ hostname: 'app.test.com',
+ cookieDomain: '.test.com',
+ formTracking: true,
+ linkClickTracking: true,
+ formTrackingConfig: { forms: { allow: ['foo'] }, fields: { allow: ['bar'] } },
+ };
standardContext = getStandardContext();
+ window.snowplow = window.snowplow || (() => {});
+ document.body.dataset.page = TEST_CATEGORY;
+
+ initUserTracking();
+ initDefaultTrackers();
});
beforeEach(() => {
getExperimentData.mockReturnValue(undefined);
getAllExperimentContexts.mockReturnValue([]);
- window.snowplow = window.snowplow || (() => {});
- window.snowplowOptions = {
- namespace: '_namespace_',
- hostname: 'app.gitfoo.com',
- cookieDomain: '.gitfoo.com',
- };
snowplowSpy = jest.spyOn(window, 'snowplow');
});
- describe('initUserTracking', () => {
- it('calls through to get a new tracker with the expected options', () => {
- initUserTracking();
- expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', {
- namespace: '_namespace_',
- hostname: 'app.gitfoo.com',
- cookieDomain: '.gitfoo.com',
- appId: '',
- userFingerprint: false,
- respectDoNotTrack: true,
- forceSecureTracker: true,
- eventMethod: 'post',
- contexts: { webPage: true, performanceTiming: true },
- formTracking: false,
- linkClickTracking: false,
- pageUnloadTimer: 10,
- formTrackingConfig: {
- fields: { allow: [] },
- forms: { allow: [] },
- },
- });
- });
- });
-
- describe('initDefaultTrackers', () => {
- beforeEach(() => {
- bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
- trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
- enableFormTracking = jest
- .spyOn(Tracking, 'enableFormTracking')
- .mockImplementation(() => null);
- setAnonymousUrlsSpy = jest.spyOn(Tracking, 'setAnonymousUrls').mockImplementation(() => null);
- });
-
- it('should activate features based on what has been enabled', () => {
- initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [standardContext]);
- expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
- expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
-
- window.snowplowOptions = {
- ...window.snowplowOptions,
- formTracking: true,
- linkClickTracking: true,
- formTrackingConfig: { forms: { whitelist: ['foo'] }, fields: { whitelist: ['bar'] } },
- };
-
- initDefaultTrackers();
- expect(enableFormTracking).toHaveBeenCalledWith(window.snowplowOptions.formTrackingConfig);
- expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking');
- });
-
- it('binds the document event handling', () => {
- initDefaultTrackers();
- expect(bindDocumentSpy).toHaveBeenCalled();
- });
-
- it('tracks page loaded events', () => {
- initDefaultTrackers();
- expect(trackLoadEventsSpy).toHaveBeenCalled();
- });
-
- it('calls the anonymized URLs method', () => {
- initDefaultTrackers();
- expect(setAnonymousUrlsSpy).toHaveBeenCalled();
- });
-
- describe('when there are experiment contexts', () => {
- const experimentContexts = [
- {
- schema: TRACKING_CONTEXT_SCHEMA,
- data: { experiment: 'experiment1', variant: 'control' },
- },
- {
- schema: TRACKING_CONTEXT_SCHEMA,
- data: { experiment: 'experiment_two', variant: 'candidate' },
- },
- ];
-
- beforeEach(() => {
- getAllExperimentContexts.mockReturnValue(experimentContexts);
- });
-
- it('includes those contexts alongside the standard context', () => {
- initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [
- standardContext,
- ...experimentContexts,
- ]);
- });
- });
- });
-
describe('.event', () => {
afterEach(() => {
window.doNotTrack = undefined;
navigator.doNotTrack = undefined;
navigator.msDoNotTrack = undefined;
+ jest.clearAllMocks();
});
it('tracks to snowplow (our current tracking system)', () => {
- Tracking.event('_category_', '_eventName_', { label: '_label_' });
+ Tracking.event(TEST_CATEGORY, TEST_ACTION, { label: TEST_LABEL });
expect(snowplowSpy).toHaveBeenCalledWith(
'trackStructEvent',
- '_category_',
- '_eventName_',
- '_label_',
+ TEST_CATEGORY,
+ TEST_ACTION,
+ TEST_LABEL,
undefined,
undefined,
[standardContext],
@@ -165,12 +79,12 @@ describe('Tracking', () => {
it('allows adding extra data to the default context', () => {
const extra = { foo: 'bar' };
- Tracking.event('_category_', '_eventName_', { extra });
+ Tracking.event(TEST_CATEGORY, TEST_ACTION, { extra });
expect(snowplowSpy).toHaveBeenCalledWith(
'trackStructEvent',
- '_category_',
- '_eventName_',
+ TEST_CATEGORY,
+ TEST_ACTION,
undefined,
undefined,
undefined,
@@ -188,28 +102,28 @@ describe('Tracking', () => {
it('skips tracking if snowplow is unavailable', () => {
window.snowplow = false;
- Tracking.event('_category_', '_eventName_');
+ Tracking.event(TEST_CATEGORY, TEST_ACTION);
expect(snowplowSpy).not.toHaveBeenCalled();
});
it('skips tracking if the user does not want to be tracked (general spec)', () => {
window.doNotTrack = '1';
- Tracking.event('_category_', '_eventName_');
+ Tracking.event(TEST_CATEGORY, TEST_ACTION);
expect(snowplowSpy).not.toHaveBeenCalled();
});
it('skips tracking if the user does not want to be tracked (firefox legacy)', () => {
navigator.doNotTrack = 'yes';
- Tracking.event('_category_', '_eventName_');
+ Tracking.event(TEST_CATEGORY, TEST_ACTION);
expect(snowplowSpy).not.toHaveBeenCalled();
});
it('skips tracking if the user does not want to be tracked (IE legacy)', () => {
navigator.msDoNotTrack = '1';
- Tracking.event('_category_', '_eventName_');
+ Tracking.event(TEST_CATEGORY, TEST_ACTION);
expect(snowplowSpy).not.toHaveBeenCalled();
});
@@ -237,7 +151,7 @@ describe('Tracking', () => {
);
});
- it('does not add empty form whitelist rules', () => {
+ it('does not add empty form allow rules', () => {
Tracking.enableFormTracking({ fields: { allow: ['input-class1'] } });
expect(snowplowSpy).toHaveBeenCalledWith(
@@ -287,7 +201,7 @@ describe('Tracking', () => {
describe('.flushPendingEvents', () => {
it('flushes any pending events', () => {
Tracking.initialized = false;
- Tracking.event('_category_', '_eventName_', { label: '_label_' });
+ Tracking.event(TEST_CATEGORY, TEST_ACTION, { label: TEST_LABEL });
expect(snowplowSpy).not.toHaveBeenCalled();
@@ -295,9 +209,9 @@ describe('Tracking', () => {
expect(snowplowSpy).toHaveBeenCalledWith(
'trackStructEvent',
- '_category_',
- '_eventName_',
- '_label_',
+ TEST_CATEGORY,
+ TEST_ACTION,
+ TEST_LABEL,
undefined,
undefined,
[standardContext],
@@ -332,14 +246,13 @@ describe('Tracking', () => {
});
});
- it('appends the hash/fragment to the pseudonymized URL', () => {
- const hash = 'first-heading';
+ it('does not appends the hash/fragment to the pseudonymized URL', () => {
window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
- window.location.hash = hash;
+ window.location.hash = 'first-heading';
Tracking.setAnonymousUrls();
- expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', `${TEST_HOST}#${hash}`);
+ expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', TEST_HOST);
});
it('does not set the referrer URL by default', () => {
@@ -409,84 +322,79 @@ describe('Tracking', () => {
});
});
- describe.each`
- term
- ${'event'}
- ${'action'}
- `('tracking interface events with data-track-$term', ({ term }) => {
+ describe('tracking interface events with data-track-action', () => {
let eventSpy;
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
- Tracking.bindDocument('_category_'); // only happens once
setHTMLFixture(`
- <input data-track-${term}="click_input1" data-track-label="_label_" value=0 />
- <input data-track-${term}="click_input2" data-track-value=0 value=0/>
- <input type="checkbox" data-track-${term}="toggle_checkbox" value=1 checked/>
- <input class="dropdown" data-track-${term}="toggle_dropdown"/>
- <div data-track-${term}="nested_event"><span class="nested"></span></div>
- <input data-track-bogus="click_bogusinput" data-track-label="_label_" value="_value_"/>
- <input data-track-${term}="click_input3" data-track-experiment="example" value="_value_"/>
- <input data-track-${term}="event_with_extra" data-track-extra='{ "foo": "bar" }' />
- <input data-track-${term}="event_with_invalid_extra" data-track-extra="invalid_json" />
+ <input data-track-action="click_input1" data-track-label="button" value="0" />
+ <input data-track-action="click_input2" data-track-value="0" value="0" />
+ <input type="checkbox" data-track-action="toggle_checkbox" value=1 checked />
+ <input class="dropdown" data-track-action="toggle_dropdown"/>
+ <div data-track-action="nested_event"><span class="nested"></span></div>
+ <input data-track-bogus="click_bogusinput" data-track-label="button" value="1" />
+ <input data-track-action="click_input3" data-track-experiment="example" value="1" />
+ <input data-track-action="event_with_extra" data-track-extra='{ "foo": "bar" }' />
+ <input data-track-action="event_with_invalid_extra" data-track-extra="invalid_json" />
`);
});
- it(`binds to clicks on elements matching [data-track-${term}]`, () => {
- document.querySelector(`[data-track-${term}="click_input1"]`).click();
+ it(`binds to clicks on elements matching [data-track-action]`, () => {
+ document.querySelector(`[data-track-action="click_input1"]`).click();
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', {
- label: '_label_',
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'click_input1', {
+ label: TEST_LABEL,
value: '0',
});
});
- it(`does not bind to clicks on elements without [data-track-${term}]`, () => {
+ it(`does not bind to clicks on elements without [data-track-action]`, () => {
document.querySelector('[data-track-bogus="click_bogusinput"]').click();
expect(eventSpy).not.toHaveBeenCalled();
});
it('allows value override with the data-track-value attribute', () => {
- document.querySelector(`[data-track-${term}="click_input2"]`).click();
+ document.querySelector(`[data-track-action="click_input2"]`).click();
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', {
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'click_input2', {
value: '0',
});
});
it('handles checkbox values correctly', () => {
- const checkbox = document.querySelector(`[data-track-${term}="toggle_checkbox"]`);
+ const checkbox = document.querySelector(`[data-track-action="toggle_checkbox"]`);
checkbox.click(); // unchecking
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'toggle_checkbox', {
value: 0,
});
checkbox.click(); // checking
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'toggle_checkbox', {
value: '1',
});
});
it('handles bootstrap dropdowns', () => {
- const dropdown = document.querySelector(`[data-track-${term}="toggle_dropdown"]`);
+ const dropdown = document.querySelector(`[data-track-action="toggle_dropdown"]`);
dropdown.dispatchEvent(new Event('show.bs.dropdown', { bubbles: true }));
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {});
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'toggle_dropdown_show', {});
dropdown.dispatchEvent(new Event('hide.bs.dropdown', { bubbles: true }));
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {});
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'toggle_dropdown_hide', {});
});
it('handles nested elements inside an element with tracking', () => {
document.querySelector('span.nested').click();
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'nested_event', {});
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'nested_event', {});
});
it('includes experiment data if linked to an experiment', () => {
@@ -497,54 +405,50 @@ describe('Tracking', () => {
};
getExperimentData.mockReturnValue(mockExperimentData);
- document.querySelector(`[data-track-${term}="click_input3"]`).click();
+ document.querySelector(`[data-track-action="click_input3"]`).click();
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input3', {
- value: '_value_',
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'click_input3', {
+ value: '1',
context: { schema: TRACKING_CONTEXT_SCHEMA, data: mockExperimentData },
});
});
it('supports extra data as JSON', () => {
- document.querySelector(`[data-track-${term}="event_with_extra"]`).click();
+ document.querySelector(`[data-track-action="event_with_extra"]`).click();
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_extra', {
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'event_with_extra', {
extra: { foo: 'bar' },
});
});
it('ignores extra if provided JSON is invalid', () => {
- document.querySelector(`[data-track-${term}="event_with_invalid_extra"]`).click();
+ document.querySelector(`[data-track-action="event_with_invalid_extra"]`).click();
- expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_invalid_extra', {});
+ expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, 'event_with_invalid_extra', {});
});
});
- describe.each`
- term
- ${'event'}
- ${'action'}
- `('tracking page loaded events with -$term', ({ term }) => {
+ describe('tracking page loaded events with -action', () => {
let eventSpy;
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
setHTMLFixture(`
- <div data-track-${term}="click_link" data-track-label="all_nested_links">
- <input data-track-${term}="render" data-track-label="label1" value=1 data-track-property="_property_"/>
- <span data-track-${term}="render" data-track-label="label2" data-track-value=1>
+ <div data-track-action="click_link" data-track-label="all_nested_links">
+ <input data-track-action="render" data-track-label="label1" value=1 data-track-property="_property_" />
+ <span data-track-action="render" data-track-label="label2" data-track-value="1">
<a href="#" id="link">Something</a>
</span>
- <input data-track-${term}="_render_bogus_" data-track-label="label3" value="_value_" data-track-property="_property_"/>
+ <input data-track-action="_render_bogus_" data-track-label="label3" value="_value_" data-track-property="_property_" />
</div>
`);
- Tracking.trackLoadEvents('_category_'); // only happens once
+ Tracking.trackLoadEvents(TEST_CATEGORY);
});
- it(`sends tracking events when [data-track-${term}="render"] is on an element`, () => {
+ it(`sends tracking events when [data-track-action="render"] is on an element`, () => {
expect(eventSpy.mock.calls).toEqual([
[
- '_category_',
+ TEST_CATEGORY,
'render',
{
label: 'label1',
@@ -553,7 +457,7 @@ describe('Tracking', () => {
},
],
[
- '_category_',
+ TEST_CATEGORY,
'render',
{
label: 'label2',
@@ -576,16 +480,16 @@ describe('Tracking', () => {
eventSpy.mockClear();
});
- it(`avoids using ancestor [data-track-${term}="render"] tracking configurations`, () => {
+ it(`avoids using ancestor [data-track-action="render"] tracking configurations`, () => {
link.dispatchEvent(new Event(event, { bubbles: true }));
expect(eventSpy).not.toHaveBeenCalledWith(
- '_category_',
+ TEST_CATEGORY,
`render${actionSuffix}`,
expect.any(Object),
);
expect(eventSpy).toHaveBeenCalledWith(
- '_category_',
+ TEST_CATEGORY,
`click_link${actionSuffix}`,
expect.objectContaining({ label: 'all_nested_links' }),
);
diff --git a/spec/frontend/tracking/utils_spec.js b/spec/frontend/tracking/utils_spec.js
new file mode 100644
index 00000000000..d6f2c5095b4
--- /dev/null
+++ b/spec/frontend/tracking/utils_spec.js
@@ -0,0 +1,99 @@
+import {
+ renameKey,
+ getReferrersCache,
+ addExperimentContext,
+ addReferrersCacheEntry,
+ filterOldReferrersCacheEntries,
+} from '~/tracking/utils';
+import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
+import { REFERRER_TTL, URLS_CACHE_STORAGE_KEY } from '~/tracking/constants';
+import { TEST_HOST } from 'helpers/test_constants';
+
+jest.mock('~/experimentation/utils', () => ({
+ getExperimentData: jest.fn().mockReturnValue({}),
+}));
+
+describe('~/tracking/utils', () => {
+ beforeEach(() => {
+ window.gl = window.gl || {};
+ window.gl.snowplowStandardContext = {};
+ });
+
+ describe('addExperimentContext', () => {
+ const options = {
+ category: 'root:index',
+ action: 'generic',
+ };
+
+ it('returns same options if no experiment is provided', () => {
+ expect(addExperimentContext({ options })).toStrictEqual({ options });
+ });
+
+ it('adds experiment if provided', () => {
+ const experiment = 'TEST_EXPERIMENT_NAME';
+
+ expect(addExperimentContext({ experiment, ...options })).toStrictEqual({
+ ...options,
+ context: { data: {}, schema: TRACKING_CONTEXT_SCHEMA },
+ });
+ });
+ });
+
+ describe('renameKey', () => {
+ it('renames a given key', () => {
+ expect(renameKey({ allow: [] }, 'allow', 'permit')).toStrictEqual({ permit: [] });
+ });
+ });
+
+ describe('referrers cache', () => {
+ describe('filterOldReferrersCacheEntries', () => {
+ it('removes entries with old or no timestamp', () => {
+ const now = Date.now();
+ const cache = [{ timestamp: now }, { timestamp: now - REFERRER_TTL }, { referrer: '' }];
+
+ expect(filterOldReferrersCacheEntries(cache)).toStrictEqual([{ timestamp: now }]);
+ });
+ });
+
+ describe('getReferrersCache', () => {
+ beforeEach(() => {
+ localStorage.removeItem(URLS_CACHE_STORAGE_KEY);
+ });
+
+ it('returns an empty array if cache is not found', () => {
+ expect(getReferrersCache()).toHaveLength(0);
+ });
+
+ it('returns an empty array if cache is invalid', () => {
+ localStorage.setItem(URLS_CACHE_STORAGE_KEY, 'Invalid JSON');
+
+ expect(getReferrersCache()).toHaveLength(0);
+ });
+
+ it('returns parsed entries if valid', () => {
+ localStorage.setItem(
+ URLS_CACHE_STORAGE_KEY,
+ JSON.stringify([{ referrer: '', timestamp: Date.now() }]),
+ );
+
+ expect(getReferrersCache()).toHaveLength(1);
+ });
+ });
+
+ describe('addReferrersCacheEntry', () => {
+ it('unshifts entry and adds timestamp', () => {
+ const now = Date.now();
+
+ addReferrersCacheEntry([{ referrer: '', originalUrl: TEST_HOST, timestamp: now }], {
+ referrer: TEST_HOST,
+ });
+
+ const cache = getReferrersCache();
+
+ expect(cache).toHaveLength(2);
+ expect(cache[0].referrer).toBe(TEST_HOST);
+ expect(cache[0].timestamp).toBeDefined();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index c5adbe9bb09..59edde48eab 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { memoize, cloneDeep } from 'lodash';
-import { getFixture, getJSONFixture } from 'helpers/fixtures';
+import usersFixture from 'test_fixtures/autocomplete/users.json';
+import { getFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import UsersSelect from '~/users_select';
@@ -15,7 +16,7 @@ const getUserSearchHTML = memoize((fixturePath) => {
return el.outerHTML;
});
-const getUsersFixture = memoize(() => getJSONFixture('autocomplete/users.json'));
+const getUsersFixture = () => usersFixture;
export const getUsersFixtureAt = (idx) => getUsersFixture()[idx];
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 ef712ec23a6..c9dea4394f9 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
@@ -61,9 +61,7 @@ describe('MRWidget approvals summary', () => {
it('render message', () => {
const names = toNounSeriesText(testRulesLeft());
- expect(wrapper.text()).toContain(
- `Requires ${TEST_APPROVALS_LEFT} more approvals from ${names}.`,
- );
+ expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} approvals from ${names}.`);
});
});
@@ -75,7 +73,9 @@ describe('MRWidget approvals summary', () => {
});
it('renders message', () => {
- expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} more approvals.`);
+ expect(wrapper.text()).toContain(
+ `Requires ${TEST_APPROVALS_LEFT} approvals from eligible users`,
+ );
});
});
diff --git a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js b/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
new file mode 100644
index 00000000000..d5d779d7a34
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
@@ -0,0 +1,35 @@
+import { GlButton, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Actions from '~/vue_merge_request_widget/components/extensions/actions.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = shallowMount(Actions, {
+ propsData: { ...propsData, widget: 'test' },
+ });
+}
+
+describe('MR widget extension actions', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('tertiaryButtons', () => {
+ it('renders buttons', () => {
+ factory({
+ tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
+ });
+
+ expect(wrapper.findAllComponents(GlButton)).toHaveLength(1);
+ });
+
+ it('renders tertiary actions in dropdown', () => {
+ factory({
+ tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
+ });
+
+ expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
index 8f6fe3cd37a..63df63a9b00 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
+++ b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
@@ -1,4 +1,7 @@
-import { registerExtension, extensions } from '~/vue_merge_request_widget/components/extensions';
+import {
+ registerExtension,
+ registeredExtensions,
+} from '~/vue_merge_request_widget/components/extensions';
import ExtensionBase from '~/vue_merge_request_widget/components/extensions/base.vue';
describe('MR widget extension registering', () => {
@@ -14,7 +17,7 @@ describe('MR widget extension registering', () => {
},
});
- expect(extensions[0]).toEqual(
+ expect(registeredExtensions.extensions[0]).toEqual(
expect.objectContaining({
extends: ExtensionBase,
name: 'Test',
diff --git a/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js b/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js
new file mode 100644
index 00000000000..f3aa5bb774f
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js
@@ -0,0 +1,36 @@
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = shallowMount(StatusIcon, {
+ propsData,
+ });
+}
+
+describe('MR widget extensions status icon', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders loading icon', () => {
+ factory({ name: 'test', isLoading: true, iconName: 'failed' });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders status icon', () => {
+ factory({ name: 'test', isLoading: false, iconName: 'failed' });
+
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('status-failed');
+ });
+
+ it('sets aria-label for status icon', () => {
+ factory({ name: 'test', isLoading: false, iconName: 'failed' });
+
+ expect(wrapper.findComponent(GlIcon).props('ariaLabel')).toBe('Failed test');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
index 5ec719b17d6..efe2bf75c3f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import DeploymentList from '~/vue_merge_request_widget/components/deployment/deployment_list.vue';
@@ -12,12 +13,14 @@ describe('MrWidgetPipelineContainer', () => {
let mock;
const factory = (props = {}) => {
- wrapper = mount(MrWidgetPipelineContainer, {
- propsData: {
- mr: { ...mockStore },
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mount(MrWidgetPipelineContainer, {
+ propsData: {
+ mr: { ...mockStore },
+ ...props,
+ },
+ }),
+ );
};
beforeEach(() => {
@@ -30,6 +33,7 @@ describe('MrWidgetPipelineContainer', () => {
});
const findDeploymentList = () => wrapper.findComponent(DeploymentList);
+ const findCIErrorMessage = () => wrapper.findByTestId('ci-error-message');
describe('when pre merge', () => {
beforeEach(() => {
@@ -69,15 +73,21 @@ describe('MrWidgetPipelineContainer', () => {
beforeEach(() => {
factory({
isPostMerge: true,
+ mr: {
+ ...mockStore,
+ pipeline: {},
+ ciStatus: undefined,
+ },
});
});
it('renders pipeline', () => {
expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(findCIErrorMessage().exists()).toBe(false);
expect(wrapper.find(MrWidgetPipeline).props()).toMatchObject({
pipeline: mockStore.mergePipeline,
pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
- ciStatus: mockStore.ciStatus,
+ ciStatus: mockStore.mergePipeline.details.status.text,
hasCi: mockStore.hasCI,
sourceBranch: mockStore.targetBranch,
sourceBranchLink: mockStore.targetBranch,
@@ -92,7 +102,6 @@ describe('MrWidgetPipelineContainer', () => {
targetBranch: 'Foo<script>alert("XSS")</script>',
},
});
-
expect(wrapper.find(MrWidgetPipeline).props().sourceBranchLink).toBe('Foo');
});
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 b31a75f30d3..2ff94a547f4 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,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
describe('Commits header component', () => {
@@ -6,6 +7,9 @@ describe('Commits header component', () => {
const createComponent = (props) => {
wrapper = shallowMount(CommitsHeader, {
+ stubs: {
+ GlSprintf,
+ },
propsData: {
isSquashEnabled: false,
targetBranch: 'main',
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 e41fb815c8d..f0fbb1d5851 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
@@ -45,6 +45,8 @@ const createTestMr = (customConfig) => {
preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY,
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
+ transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition),
+ translateStateToMachine: () => this.transitionStateMachine(),
};
Object.assign(mr, customConfig.mr);
@@ -304,6 +306,9 @@ describe('ReadyToMerge', () => {
setImmediate(() => {
expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
+ transition: 'start-auto-merge',
+ });
const params = wrapper.vm.service.merge.mock.calls[0][0];
@@ -341,10 +346,15 @@ describe('ReadyToMerge', () => {
it('should handle merge action accepted case', (done) => {
createComponent();
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('success'));
jest.spyOn(wrapper.vm, 'initiateMergePolling').mockImplementation(() => {});
wrapper.vm.handleMergeButtonClick();
+ expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
+ transition: 'start-merge',
+ });
+
setImmediate(() => {
expect(wrapper.vm.isMakingRequest).toBeTruthy();
expect(wrapper.vm.initiateMergePolling).toHaveBeenCalled();
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 61e44140efc..be15e4df66d 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
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import createFlash from '~/flash';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
+import toast from '~/vue_shared/plugins/global_toast';
import eventHub from '~/vue_merge_request_widget/event_hub';
-jest.mock('~/flash');
+jest.mock('~/vue_shared/plugins/global_toast');
const createComponent = () => {
const Component = Vue.extend(WorkInProgress);
@@ -63,10 +63,7 @@ describe('Wip', () => {
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- expect(createFlash).toHaveBeenCalledWith({
- message: 'Marked as ready. Merging is now allowed.',
- type: 'notice',
- });
+ expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.');
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index f356f6fb5bf..34a741cf8f2 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -280,7 +280,7 @@ export default {
merge_train_index: 1,
security_reports_docs_path: 'security-reports-docs-path',
sast_comparison_path: '/sast_comparison_path',
- secret_scanning_comparison_path: '/secret_scanning_comparison_path',
+ secret_detection_comparison_path: '/secret_detection_comparison_path',
gitpod_enabled: true,
show_gitpod_button: true,
gitpod_url: 'http://gitpod.localhost',
diff --git a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
index bd22183cbea..913d5860b48 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js
@@ -8,11 +8,9 @@ describe('MRWidgetHowToMerge', () => {
function mountComponent({ data = {}, props = {} } = {}) {
wrapper = shallowMount(MrWidgetHowToMergeModal, {
data() {
- return { ...data };
- },
- propsData: {
- ...props,
+ return data;
},
+ propsData: props,
stubs: {},
});
}
@@ -57,4 +55,16 @@ describe('MRWidgetHowToMerge', () => {
mountComponent({ props: { isFork: true } });
expect(findInstructionsFields().at(0).text()).toContain('FETCH_HEAD');
});
+
+ it('escapes the target branch name shell-secure', () => {
+ mountComponent({ props: { targetBranch: '";echo$IFS"you_shouldnt_run_this' } });
+
+ expect(findInstructionsFields().at(1).text()).toContain('\'";echo$IFS"you_shouldnt_run_this\'');
+ });
+
+ it('escapes the source branch name shell-secure', () => {
+ mountComponent({ props: { sourceBranch: 'branch-of-$USER' } });
+
+ expect(findInstructionsFields().at(0).text()).toContain("'branch-of-$USER'");
+ });
});
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 c50cf7cb076..5aba6982886 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1,13 +1,19 @@
+import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
import SmartInterval from '~/smart_interval';
+import {
+ registerExtension,
+ registeredExtensions,
+} from '~/vue_merge_request_widget/components/extensions';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
@@ -15,6 +21,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import mockData from './mock_data';
+import testExtension from './test_extension';
jest.mock('~/smart_interval');
@@ -879,4 +886,48 @@ describe('MrWidgetOptions', () => {
});
});
});
+
+ describe('mock extension', () => {
+ beforeEach(() => {
+ registerExtension(testExtension);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ registeredExtensions.extensions = [];
+ });
+
+ it('renders collapsed data', async () => {
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Test extension summary count: 1');
+ });
+
+ it('renders full data', async () => {
+ await waitForPromises();
+
+ wrapper
+ .find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
+ .trigger('click');
+
+ await Vue.nextTick();
+
+ const collapsedSection = wrapper.find('[data-testid="widget-extension-collapsed-section"]');
+ expect(collapsedSection.exists()).toBe(true);
+ expect(collapsedSection.text()).toContain('Hello world');
+
+ // Renders icon in the row
+ expect(collapsedSection.find(GlIcon).exists()).toBe(true);
+ expect(collapsedSection.find(GlIcon).props('name')).toBe('status-failed');
+
+ // Renders badge in the row
+ expect(collapsedSection.find(GlBadge).exists()).toBe(true);
+ expect(collapsedSection.find(GlBadge).text()).toBe('Closed');
+
+ // Renders a link in the row
+ expect(collapsedSection.find(GlLink).exists()).toBe(true);
+ expect(collapsedSection.find(GlLink).text()).toBe('GitLab.com');
+ });
+ });
});
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 bf0179aa425..febcfcd4019 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
@@ -162,7 +162,7 @@ describe('MergeRequestStore', () => {
expect(store.securityReportsDocsPath).toBe('security-reports-docs-path');
});
- it.each(['sast_comparison_path', 'secret_scanning_comparison_path'])(
+ it.each(['sast_comparison_path', 'secret_detection_comparison_path'])(
'should set %s path',
(property) => {
// Ensure something is set in the mock data
diff --git a/spec/frontend/vue_mr_widget/test_extension.js b/spec/frontend/vue_mr_widget/test_extension.js
new file mode 100644
index 00000000000..a29a4d2fb46
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/test_extension.js
@@ -0,0 +1,37 @@
+import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
+
+export default {
+ name: 'WidgetTestExtension',
+ props: ['targetProjectFullPath'],
+ computed: {
+ summary({ count, targetProjectFullPath }) {
+ return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
+ },
+ statusIcon({ count }) {
+ return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
+ },
+ },
+ methods: {
+ fetchCollapsedData({ targetProjectFullPath }) {
+ return Promise.resolve({ targetProjectFullPath, count: 1 });
+ },
+ fetchFullData() {
+ return Promise.resolve([
+ {
+ id: 1,
+ text: 'Hello world',
+ icon: {
+ name: EXTENSION_ICONS.failed,
+ },
+ badge: {
+ text: 'Closed',
+ },
+ link: {
+ href: 'https://gitlab.com',
+ text: 'GitLab.com',
+ },
+ },
+ ]);
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index c7758b0faef..44b4c0398cd 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -4,12 +4,12 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-dropdown-stub
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
right="true"
- showhighlighteditemstitle="true"
size="medium"
text="Clone"
variant="info"
@@ -35,6 +35,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<b-form-input-stub
class="gl-form-input"
debounce="0"
+ formatter="[Function]"
readonly="true"
type="text"
value="ssh://foo.bar"
@@ -78,6 +79,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<b-form-input-stub
class="gl-form-input"
debounce="0"
+ formatter="[Function]"
readonly="true"
type="text"
value="http://foo.bar"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index f2ff12b2acd..2b89e36344d 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -4,12 +4,12 @@ exports[`SplitButton renders actionItems 1`] = `
<gl-dropdown-stub
category="primary"
clearalltext="Clear all"
+ clearalltextclass="gl-px-5"
headertext=""
hideheaderborder="true"
highlighteditemstitle="Selected"
highlighteditemstitleclass="gl-px-5"
menu-class=""
- showhighlighteditemstitle="true"
size="medium"
split="true"
text="professor"
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index c6c351a7f3f..3277aab43f0 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -1,25 +1,16 @@
import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
-import SourceEditor from '~/vue_shared/components/source_editor.vue';
describe('Blob Simple Viewer component', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
- function createComponent(
- content = contentMock,
- isRawContent = false,
- isRefactorFlagEnabled = false,
- ) {
+ function createComponent(content = contentMock, isRawContent = false) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
- glFeatures: {
- refactorBlobViewer: isRefactorFlagEnabled,
- },
},
propsData: {
content,
@@ -94,32 +85,4 @@ describe('Blob Simple Viewer component', () => {
});
});
});
-
- describe('Vue refactoring to use Source Editor', () => {
- const findSourceEditor = () => wrapper.find(SourceEditor);
-
- it.each`
- doesRender | condition | isRawContent | isRefactorFlagEnabled
- ${'Does not'} | ${'rawContent is not specified'} | ${false} | ${true}
- ${'Does not'} | ${'feature flag is disabled is not specified'} | ${true} | ${false}
- ${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false}
- ${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true}
- `(
- '$doesRender render Source Editor component in readonly mode when $condition',
- async ({ isRawContent, isRefactorFlagEnabled } = {}) => {
- createComponent('raw content', isRawContent, isRefactorFlagEnabled);
- await waitForPromises();
-
- if (isRawContent && isRefactorFlagEnabled) {
- expect(findSourceEditor().exists()).toBe(true);
-
- expect(findSourceEditor().props('value')).toBe('raw content');
- expect(findSourceEditor().props('fileName')).toBe('test.js');
- expect(findSourceEditor().props('editorOptions')).toEqual({ readOnly: true });
- } else {
- expect(findSourceEditor().exists()).toBe(false);
- }
- },
- );
- });
});
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index d30f36ec63c..fef50bdaccc 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -111,15 +111,13 @@ describe('ColorPicker', () => {
gon.suggested_label_colors = {};
createComponent(shallowMount);
- expect(description()).toBe('Choose any color');
+ expect(description()).toBe('Enter any color.');
expect(presetColors().exists()).toBe(false);
});
it('shows the suggested colors', () => {
createComponent(shallowMount);
- expect(description()).toBe(
- 'Choose any color. Or you can choose one of the suggested colors below',
- );
+ expect(description()).toBe('Enter any color or choose one of the suggested colors below.');
expect(presetColors()).toHaveLength(4);
});
diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
index 175d79dd1c2..194681a6138 100644
--- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlAlert, GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import Component from '~/vue_shared/components/dismissible_feedback_alert.vue';
@@ -8,20 +8,13 @@ describe('Dismissible Feedback Alert', () => {
let wrapper;
- const defaultProps = {
- featureName: 'Dependency List',
- feedbackLink: 'https://gitlab.link',
- };
-
+ const featureName = 'Dependency List';
const STORAGE_DISMISSAL_KEY = 'dependency_list_feedback_dismissed';
- const createComponent = ({ props, shallow } = {}) => {
- const mountFn = shallow ? shallowMount : mount;
-
+ const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = mountFn(Component, {
propsData: {
- ...defaultProps,
- ...props,
+ featureName,
},
stubs: {
GlSprintf,
@@ -34,8 +27,8 @@ describe('Dismissible Feedback Alert', () => {
wrapper = null;
});
- const findAlert = () => wrapper.find(GlAlert);
- const findLink = () => wrapper.find(GlLink);
+ const createFullComponent = () => createComponent({ mountFn: mount });
+ const findAlert = () => wrapper.findComponent(GlAlert);
describe('with default', () => {
beforeEach(() => {
@@ -46,17 +39,6 @@ describe('Dismissible Feedback Alert', () => {
expect(findAlert().exists()).toBe(true);
});
- it('contains feature name', () => {
- expect(findAlert().text()).toContain(defaultProps.featureName);
- });
-
- it('contains provided link', () => {
- const link = findLink();
-
- expect(link.attributes('href')).toBe(defaultProps.feedbackLink);
- expect(link.attributes('target')).toBe('_blank');
- });
-
it('should have the storage key set', () => {
expect(wrapper.vm.storageKey).toBe(STORAGE_DISMISSAL_KEY);
});
@@ -65,7 +47,7 @@ describe('Dismissible Feedback Alert', () => {
describe('dismissible', () => {
describe('after dismissal', () => {
beforeEach(() => {
- createComponent({ shallow: false });
+ createFullComponent();
findAlert().vm.$emit('dismiss');
});
@@ -81,7 +63,7 @@ describe('Dismissible Feedback Alert', () => {
describe('already dismissed', () => {
it('should not show the alert once dismissed', async () => {
localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true');
- createComponent({ shallow: false });
+ createFullComponent();
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
new file mode 100644
index 00000000000..996df34f2ff
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -0,0 +1,141 @@
+import { shallowMount } from '@vue/test-utils';
+import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+import { UP_KEY_CODE, DOWN_KEY_CODE, TAB_KEY_CODE } from '~/lib/utils/keycodes';
+
+const MOCK_INDEX = 0;
+const MOCK_MAX = 10;
+const MOCK_MIN = 0;
+const MOCK_DEFAULT_INDEX = 0;
+
+describe('DropdownKeyboardNavigation', () => {
+ let wrapper;
+
+ const defaultProps = {
+ index: MOCK_INDEX,
+ max: MOCK_MAX,
+ min: MOCK_MIN,
+ defaultIndex: MOCK_DEFAULT_INDEX,
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(DropdownKeyboardNavigation, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const helpers = {
+ arrowDown: () => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: DOWN_KEY_CODE }));
+ },
+ arrowUp: () => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: UP_KEY_CODE }));
+ },
+ tab: () => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: TAB_KEY_CODE }));
+ },
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('onInit', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should $emit @change with the default index', async () => {
+ expect(wrapper.emitted('change')[0]).toStrictEqual([MOCK_DEFAULT_INDEX]);
+ });
+
+ it('should $emit @change with the default index when max changes', async () => {
+ wrapper.setProps({ max: 20 });
+ await wrapper.vm.$nextTick();
+ // The first @change`call happens on created() so we test for the second [1]
+ expect(wrapper.emitted('change')[1]).toStrictEqual([MOCK_DEFAULT_INDEX]);
+ });
+ });
+
+ describe('keydown events', () => {
+ let incrementSpy;
+
+ beforeEach(() => {
+ createComponent();
+ incrementSpy = jest.spyOn(wrapper.vm, 'increment');
+ });
+
+ afterEach(() => {
+ incrementSpy.mockRestore();
+ });
+
+ it('onKeydown-Down calls increment(1)', () => {
+ helpers.arrowDown();
+
+ expect(incrementSpy).toHaveBeenCalledWith(1);
+ });
+
+ it('onKeydown-Up calls increment(-1)', () => {
+ helpers.arrowUp();
+
+ expect(incrementSpy).toHaveBeenCalledWith(-1);
+ });
+
+ it('onKeydown-Tab $emits @tab event', () => {
+ helpers.tab();
+
+ expect(wrapper.emitted('tab')).toHaveLength(1);
+ });
+ });
+
+ describe('increment', () => {
+ describe('when max is 0', () => {
+ beforeEach(() => {
+ createComponent({ max: 0 });
+ });
+
+ it('does not $emit any @change events', () => {
+ helpers.arrowDown();
+
+ // The first @change`call happens on created() so we test that we only have 1 call
+ expect(wrapper.emitted('change')).toHaveLength(1);
+ });
+ });
+
+ describe.each`
+ keyboardAction | direction | index | max | min
+ ${helpers.arrowDown} | ${1} | ${10} | ${10} | ${0}
+ ${helpers.arrowUp} | ${-1} | ${0} | ${10} | ${0}
+ `('moving out of bounds', ({ keyboardAction, direction, index, max, min }) => {
+ beforeEach(() => {
+ createComponent({ index, max, min });
+ keyboardAction();
+ });
+
+ it(`in ${direction} direction does not $emit any @change events`, () => {
+ // The first @change`call happens on created() so we test that we only have 1 call
+ expect(wrapper.emitted('change')).toHaveLength(1);
+ });
+ });
+
+ describe.each`
+ keyboardAction | direction | index | max | min
+ ${helpers.arrowDown} | ${1} | ${0} | ${10} | ${0}
+ ${helpers.arrowUp} | ${-1} | ${10} | ${10} | ${0}
+ `('moving in bounds', ({ keyboardAction, direction, index, max, min }) => {
+ beforeEach(() => {
+ createComponent({ index, max, min });
+ keyboardAction();
+ });
+
+ it(`in ${direction} direction $emits @change event with the correct index ${
+ index + direction
+ }`, () => {
+ // The first @change`call happens on created() so we test for the second [1]
+ expect(wrapper.emitted('change')[1]).toStrictEqual([index + direction]);
+ });
+ });
+ });
+});
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 134c6c8b929..ae02c554e13 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
@@ -141,7 +141,62 @@ export const mockEpicToken = {
token: EpicToken,
operators: OPERATOR_IS_ONLY,
idProperty: 'iid',
- fetchEpics: () => Promise.resolve({ data: mockEpics }),
+ fullPath: 'gitlab-org',
+};
+
+export const mockEpicNode1 = {
+ __typename: 'Epic',
+ parent: null,
+ id: 'gid://gitlab/Epic/40',
+ iid: '2',
+ title: 'Marketing epic',
+ description: 'Mock epic description',
+ state: 'opened',
+ startDate: '2017-12-25',
+ dueDate: '2018-02-15',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/1',
+ hasChildren: false,
+ hasParent: false,
+ confidential: false,
+};
+
+export const mockEpicNode2 = {
+ __typename: 'Epic',
+ parent: null,
+ id: 'gid://gitlab/Epic/41',
+ iid: '3',
+ title: 'Another marketing',
+ startDate: '2017-12-26',
+ dueDate: '2018-03-10',
+ state: 'opened',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/marketing/-/epics/2',
+};
+
+export const mockGroupEpicsQueryResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/1',
+ name: 'Gitlab Org',
+ epics: {
+ edges: [
+ {
+ node: {
+ ...mockEpicNode1,
+ },
+ __typename: 'EpicEdge',
+ },
+ {
+ node: {
+ ...mockEpicNode2,
+ },
+ __typename: 'EpicEdge',
+ },
+ ],
+ __typename: 'EpicConnection',
+ },
+ __typename: 'Group',
+ },
+ },
};
export const mockReactionEmojiToken = {
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 d3e1bfef561..14fcffd3c50 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
@@ -57,7 +57,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
data() {
return { ...data };
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index eb1dbed52cc..f9ce0338d2f 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -67,7 +67,7 @@ function createComponent({
provide: {
portalName: 'fake target',
alignSuggestions: jest.fn(),
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
slots,
@@ -206,26 +206,50 @@ describe('BaseToken', () => {
describe('events', () => {
let wrapperWithNoStubs;
- beforeEach(() => {
- wrapperWithNoStubs = createComponent({
- stubs: { Portal: true },
- });
- });
-
afterEach(() => {
wrapperWithNoStubs.destroy();
});
- it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
- jest.useFakeTimers();
+ describe('when activeToken has been selected', () => {
+ beforeEach(() => {
+ wrapperWithNoStubs = createComponent({
+ props: {
+ ...mockProps,
+ getActiveTokenValue: () => ({ title: '' }),
+ suggestionsLoading: true,
+ },
+ stubs: { Portal: true },
+ });
+ });
+ it('does not emit `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
+ jest.useFakeTimers();
- wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
- await wrapperWithNoStubs.vm.$nextTick();
+ wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
+ await wrapperWithNoStubs.vm.$nextTick();
- jest.runAllTimers();
+ jest.runAllTimers();
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy();
- expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toEqual([['']]);
+ });
+ });
+
+ describe('when activeToken has not been selected', () => {
+ beforeEach(() => {
+ wrapperWithNoStubs = createComponent({
+ stubs: { Portal: true },
+ });
+ });
+ it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => {
+ jest.useFakeTimers();
+
+ wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' });
+ await wrapperWithNoStubs.vm.$nextTick();
+
+ jest.runAllTimers();
+
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy();
+ expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 09eac636cae..f3e8b2d0c1b 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -42,7 +42,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index c2d61fd9f05..36071c900df 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -48,7 +48,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
index 68ed46fc3a2..6ee5d50d396 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
@@ -1,15 +1,21 @@
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import { GlFilteredSearchTokenSegment } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import searchEpicsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_epics.query.graphql';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { mockEpicToken, mockEpics } from '../mock_data';
+import { mockEpicToken, mockEpics, mockGroupEpicsQueryResponse } from '../mock_data';
jest.mock('~/flash');
+Vue.use(VueApollo);
const defaultStubs = {
Portal: true,
@@ -21,31 +27,39 @@ const defaultStubs = {
},
};
-function createComponent(options = {}) {
- const {
- config = mockEpicToken,
- value = { data: '' },
- active = false,
- stubs = defaultStubs,
- } = options;
- return mount(EpicToken, {
- propsData: {
- config,
- value,
- active,
- },
- provide: {
- portalName: 'fake target',
- alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
- },
- stubs,
- });
-}
-
describe('EpicToken', () => {
let mock;
let wrapper;
+ let fakeApollo;
+
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+
+ function createComponent(
+ options = {},
+ epicsQueryHandler = jest.fn().mockResolvedValue(mockGroupEpicsQueryResponse),
+ ) {
+ fakeApollo = createMockApollo([[searchEpicsQuery, epicsQueryHandler]]);
+ const {
+ config = mockEpicToken,
+ value = { data: '' },
+ active = false,
+ stubs = defaultStubs,
+ } = options;
+ return mount(EpicToken, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ config,
+ value,
+ active,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
+ },
+ stubs,
+ });
+ }
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -71,23 +85,20 @@ describe('EpicToken', () => {
describe('methods', () => {
describe('fetchEpicsBySearchTerm', () => {
- it('calls `config.fetchEpics` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics');
+ it('calls fetchEpics with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm, 'fetchEpics');
- wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
+ findBaseToken().vm.$emit('fetch-suggestions', 'foo');
- expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith({
- epicPath: '',
- search: 'foo',
- });
+ expect(wrapper.vm.fetchEpics).toHaveBeenCalledWith('foo');
});
it('sets response to `epics` when request is successful', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics').mockResolvedValue({
+ jest.spyOn(wrapper.vm, 'fetchEpics').mockResolvedValue({
data: mockEpics,
});
- wrapper.vm.fetchEpicsBySearchTerm({});
+ findBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@@ -95,9 +106,9 @@ describe('EpicToken', () => {
});
it('calls `createFlash` with flash error message when request fails', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
+ jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
- wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
+ findBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
@@ -107,9 +118,9 @@ describe('EpicToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
+ jest.spyOn(wrapper.vm, 'fetchEpics').mockRejectedValue({});
- wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
+ findBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
@@ -123,15 +134,15 @@ describe('EpicToken', () => {
beforeEach(async () => {
wrapper = createComponent({
- value: { data: `${mockEpics[0].group_full_path}::&${mockEpics[0].iid}` },
+ value: { data: `${mockEpics[0].title}::&${mockEpics[0].iid}` },
data: { epics: mockEpics },
});
await wrapper.vm.$nextTick();
});
- it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ it('renders BaseToken component', () => {
+ expect(findBaseToken().exists()).toBe(true);
});
it('renders token item when value is selected', () => {
@@ -142,9 +153,9 @@ describe('EpicToken', () => {
});
it.each`
- value | valueType | tokenValueString
- ${`${mockEpics[0].group_full_path}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
- ${`${mockEpics[1].group_full_path}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
+ value | valueType | tokenValueString
+ ${`${mockEpics[0].title}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
+ ${`${mockEpics[1].title}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
`('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => {
wrapper.setProps({
value: { data: value },
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 a609aaa1c4e..af90ee93543 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
@@ -21,7 +21,7 @@ describe('IterationToken', () => {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index a348344b9dd..f55fb2836e3 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -48,7 +48,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
listeners,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index bfb593bf82d..936841651d1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -48,7 +48,7 @@ function createComponent(options = {}) {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
stubs,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
index e788c742736..4277899f8db 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
@@ -19,7 +19,7 @@ describe('WeightToken', () => {
provide: {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
- suggestionsListClass: 'custom-class',
+ suggestionsListClass: () => 'custom-class',
},
});
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 2658fa4a706..f74b9b37197 100644
--- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -94,10 +94,6 @@ describe('IssueAssigneesComponent', () => {
expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Assigned to Terrell Graham');
});
- it('renders component root element with class `issue-assignees`', () => {
- expect(wrapper.element.classList.contains('issue-assignees')).toBe(true);
- });
-
it('renders assignee', () => {
const data = findAvatars().wrappers.map((x) => ({
...x.props(),
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index ba2450b56c9..9bc2aad1895 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -60,7 +60,7 @@ describe('Suggestion Diff component', () => {
expect(findHelpButton().exists()).toBe(true);
});
- it('renders apply suggestion and add to batch buttons', () => {
+ it('renders add to batch button when more than 1 suggestion', () => {
createComponent({
suggestionsCount: 2,
});
@@ -68,8 +68,7 @@ describe('Suggestion Diff component', () => {
const applyBtn = findApplyButton();
const addToBatchBtn = findAddToBatchButton();
- expect(applyBtn.exists()).toBe(true);
- expect(applyBtn.html().includes('Apply suggestion')).toBe(true);
+ expect(applyBtn.exists()).toBe(false);
expect(addToBatchBtn.exists()).toBe(true);
expect(addToBatchBtn.html().includes('Add suggestion to batch')).toBe(true);
@@ -85,7 +84,7 @@ describe('Suggestion Diff component', () => {
describe('when apply suggestion is clicked', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ batchSuggestionsCount: 0 });
findApplyButton().vm.$emit('apply');
});
@@ -140,11 +139,11 @@ describe('Suggestion Diff component', () => {
describe('apply suggestions is clicked', () => {
it('emits applyBatch', () => {
- createComponent({ isBatched: true });
+ createComponent({ isBatched: true, batchSuggestionsCount: 2 });
- findApplyBatchButton().vm.$emit('click');
+ findApplyButton().vm.$emit('apply');
- expect(wrapper.emitted().applyBatch).toEqual([[]]);
+ expect(wrapper.emitted().applyBatch).toEqual([[undefined]]);
});
});
@@ -155,23 +154,24 @@ describe('Suggestion Diff component', () => {
isBatched: true,
});
- const applyBatchBtn = findApplyBatchButton();
+ const applyBatchBtn = findApplyButton();
const removeFromBatchBtn = findRemoveFromBatchButton();
expect(removeFromBatchBtn.exists()).toBe(true);
expect(removeFromBatchBtn.html().includes('Remove from batch')).toBe(true);
expect(applyBatchBtn.exists()).toBe(true);
- expect(applyBatchBtn.html().includes('Apply suggestions')).toBe(true);
+ expect(applyBatchBtn.html().includes('Apply suggestion')).toBe(true);
expect(applyBatchBtn.html().includes(String('9'))).toBe(true);
});
it('hides add to batch and apply buttons', () => {
createComponent({
isBatched: true,
+ batchSuggestionsCount: 9,
});
- expect(findApplyButton().exists()).toBe(false);
+ expect(findApplyButton().exists()).toBe(true);
expect(findAddToBatchButton().exists()).toBe(false);
});
@@ -215,9 +215,8 @@ describe('Suggestion Diff component', () => {
});
it('disables apply suggestion and hides add to batch button', () => {
- expect(findApplyButton().exists()).toBe(true);
+ expect(findApplyButton().exists()).toBe(false);
expect(findAddToBatchButton().exists()).toBe(false);
- expect(findApplyButton().attributes('disabled')).toBe('true');
});
});
@@ -225,7 +224,7 @@ describe('Suggestion Diff component', () => {
const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip');
it('renders correct tooltip message when button is applicable', () => {
- createComponent();
+ createComponent({ batchSuggestionsCount: 0 });
const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true);
@@ -234,7 +233,7 @@ describe('Suggestion Diff component', () => {
it('renders the inapplicable reason in the tooltip when button is not applicable', () => {
const inapplicableReason = 'lorem';
- createComponent({ canApply: false, inapplicableReason });
+ createComponent({ canApply: false, inapplicableReason, batchSuggestionsCount: 0 });
const tooltip = findTooltip();
expect(tooltip.modifiers.viewport).toBe(true);
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
index 5bd6bda2d2c..af27e953776 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
@@ -77,7 +77,7 @@ describe('Suggestion Diff component', () => {
it.each`
event | childArgs | args
${'apply'} | ${['test-event']} | ${[{ callback: 'test-event', suggestionId }]}
- ${'applyBatch'} | ${[]} | ${[]}
+ ${'applyBatch'} | ${['test-event']} | ${['test-event']}
${'addToBatch'} | ${[]} | ${[suggestionId]}
${'removeFromBatch'} | ${[]} | ${[suggestionId]}
`('emits $event event on sugestion diff header $event', ({ event, childArgs, args }) => {
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 ab028ea52b7..1ed7844b395 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,4 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
+// eslint-disable-next-line import/no-deprecated
+import { getJSONFixture } from 'helpers/fixtures';
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';
@@ -11,6 +13,7 @@ describe('ProjectListItem component', () => {
let vm;
let options;
+ // eslint-disable-next-line import/no-deprecated
const project = getJSONFixture('static/projects.json')[0];
beforeEach(() => {
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 06b00a8e196..1f97d3ff3fa 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,6 +2,8 @@ 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 { 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';
@@ -11,6 +13,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 searchResults = allProjects.slice(0, 5);
let selected = [];
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 14e0c8a2278..d9b7cd5afa2 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
@@ -157,9 +157,9 @@ describe('LabelsSelect Mutations', () => {
beforeEach(() => {
labels = [
- { id: 1, title: 'scoped::test', set: true },
- { id: 2, set: false, title: 'scoped::one' },
- { id: 3, title: '' },
+ { id: 1, title: 'scoped' },
+ { id: 2, title: 'scoped::one', set: false },
+ { id: 3, title: 'scoped::test', set: true },
{ id: 4, title: '' },
];
});
@@ -189,9 +189,9 @@ describe('LabelsSelect Mutations', () => {
});
expect(state.labels).toEqual([
- { id: 1, title: 'scoped::test', set: false },
- { id: 2, set: true, title: 'scoped::one', touched: true },
- { id: 3, title: '' },
+ { id: 1, title: 'scoped' },
+ { id: 2, title: 'scoped::one', set: true, touched: true },
+ { id: 3, title: 'scoped::test', set: false },
{ id: 4, title: '' },
]);
});
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 843298a1406..8931584e12c 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,13 +5,14 @@ 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 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 projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import {
mockSuggestedColors,
createLabelSuccessfulResponse,
- labelsQueryResponse,
+ workspaceLabelsQueryResponse,
} from './mock_data';
jest.mock('~/flash');
@@ -47,11 +48,14 @@ describe('DropdownContentsCreateView', () => {
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
};
- const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => {
+ const createComponent = ({
+ mutationHandler = createLabelSuccessHandler,
+ issuableType = IssuableType.Issue,
+ } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
- query: projectLabelsQuery,
- data: labelsQueryResponse.data,
+ query: labelsQueries[issuableType].workspaceQuery,
+ data: workspaceLabelsQueryResponse.data,
variables: {
fullPath: '',
searchTerm: '',
@@ -61,6 +65,10 @@ describe('DropdownContentsCreateView', () => {
wrapper = shallowMount(DropdownContentsCreateView, {
localVue,
apolloProvider: mockApollo,
+ propsData: {
+ issuableType,
+ fullPath: '',
+ },
});
};
@@ -135,15 +143,6 @@ describe('DropdownContentsCreateView', () => {
expect(findCreateButton().props('disabled')).toBe(false);
});
- it('calls a mutation with correct parameters on Create button click', () => {
- findCreateButton().vm.$emit('click');
- expect(createLabelSuccessHandler).toHaveBeenCalledWith({
- color: '#009966',
- projectPath: '',
- title: 'Test title',
- });
- });
-
it('renders a loader spinner after Create button click', async () => {
findCreateButton().vm.$emit('click');
await nextTick();
@@ -162,6 +161,30 @@ describe('DropdownContentsCreateView', () => {
});
});
+ it('calls a mutation with `projectPath` variable on the issue', () => {
+ createComponent();
+ fillLabelAttributes();
+ findCreateButton().vm.$emit('click');
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: '#009966',
+ projectPath: '',
+ title: 'Test title',
+ });
+ });
+
+ it('calls a mutation with `groupPath` variable on the epic', () => {
+ createComponent({ issuableType: IssuableType.Epic });
+ fillLabelAttributes();
+ findCreateButton().vm.$emit('click');
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: '#009966',
+ groupPath: '',
+ title: 'Test title',
+ });
+ });
+
it('calls createFlash is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes();
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 537bbc8e71e..fac3331a2b8 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
@@ -1,36 +1,43 @@
-import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdownItem,
+ GlIntersectionObserver,
+} from '@gitlab/ui';
import { shallowMount, createLocalVue } 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 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';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
-import { mockConfig, labelsQueryResponse } from './mock_data';
+import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
-const selectedLabels = [
+const localSelectedLabels = [
{
- id: 28,
- title: 'Bug',
- description: 'Label for bugs',
- color: '#FF0000',
- textColor: '#FFFFFF',
+ color: '#2f7b2e',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/2',
+ title: 'Label2',
},
];
describe('DropdownContentsLabelsView', () => {
let wrapper;
- const successfulQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse);
+ const successfulQueryHandler = jest.fn().mockResolvedValue(workspaceLabelsQueryResponse);
+
+ const findFirstLabel = () => wrapper.findAllComponents(GlDropdownItem).at(0);
const createComponent = ({
initialState = mockConfig,
@@ -43,14 +50,13 @@ describe('DropdownContentsLabelsView', () => {
localVue,
apolloProvider: mockApollo,
provide: {
- projectPath: 'test',
- iid: 1,
variant: DropdownVariant.Sidebar,
...injected,
},
propsData: {
...initialState,
- selectedLabels,
+ localSelectedLabels,
+ issuableType: IssuableType.Issue,
},
stubs: {
GlSearchBoxByType,
@@ -65,23 +71,31 @@ describe('DropdownContentsLabelsView', () => {
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findLabelsList = () => wrapper.find('[data-testid="labels-list"]');
const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]');
+ async function makeObserverAppear() {
+ await findObserver().vm.$emit('appear');
+ }
+
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();
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not render labels list', async () => {
createComponent();
+ await makeObserverAppear();
expect(findLabelsList().exists()).toBe(false);
});
});
@@ -89,6 +103,7 @@ describe('DropdownContentsLabelsView', () => {
describe('when labels are loaded', () => {
beforeEach(async () => {
createComponent();
+ await makeObserverAppear();
await waitForPromises();
});
@@ -118,6 +133,7 @@ describe('DropdownContentsLabelsView', () => {
},
}),
});
+ await makeObserverAppear();
findSearchInput().vm.$emit('input', '123');
await waitForPromises();
await nextTick();
@@ -127,8 +143,26 @@ describe('DropdownContentsLabelsView', () => {
it('calls `createFlash` when fetching labels failed', async () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') });
+ await makeObserverAppear();
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises();
+
expect(createFlash).toHaveBeenCalled();
});
+
+ it('emits an `input` event on label click', async () => {
+ createComponent();
+ await makeObserverAppear();
+ await waitForPromises();
+ findFirstLabel().trigger('click');
+
+ expect(wrapper.emitted('input')[0][0]).toEqual(expect.arrayContaining(localSelectedLabels));
+ });
+
+ it('does not trigger query when component did not appear', () => {
+ createComponent();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findLabelsList().exists()).toBe(false);
+ expect(successfulQueryHandler).not.toHaveBeenCalled();
+ });
});
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 a1b40a891ec..36704ac5ef3 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
@@ -1,6 +1,5 @@
-import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-
+import { nextTick } from 'vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
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';
@@ -8,10 +7,26 @@ import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_s
import { mockLabels } from './mock_data';
+const showDropdown = jest.fn();
+
+const GlDropdownStub = {
+ template: `
+ <div data-testid="dropdown">
+ <slot name="header"></slot>
+ <slot></slot>
+ <slot name="footer"></slot>
+ </div>
+ `,
+ methods: {
+ show: showDropdown,
+ hide: jest.fn(),
+ },
+};
+
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ props = {}, injected = {} } = {}) => {
+ const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
labelsCreateTitle: 'test',
@@ -22,38 +37,112 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels',
variant: 'sidebar',
+ issuableType: 'issue',
+ fullPath: 'test',
...props,
},
+ data() {
+ return {
+ ...data,
+ };
+ },
provide: {
allowLabelCreate: true,
labelsManagePath: 'foo/bar',
...injected,
},
stubs: {
- GlDropdown,
+ GlDropdown: GlDropdownStub,
},
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
+ const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
+ const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
+ 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({
+ isVisible: true,
+ });
+
+ expect(findDropdown().emitted('show')).toBeUndefined();
+ });
+
+ it('does not emit `setLabels` event on dropdown hide if labels did not change', () => {
+ createComponent();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setLabels')).toBeUndefined();
+ });
+
+ it('emits `setLabels` event on dropdown hide if labels changed on non-sidebar widget', async () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone } });
+ const updatedLabel = {
+ id: 28,
+ title: 'Bug',
+ description: 'Label for bugs',
+ color: '#FF0000',
+ textColor: '#FFFFFF',
+ };
+ findLabelsView().vm.$emit('input', [updatedLabel]);
+ await nextTick();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]);
+ });
+
+ it('emits `setLabels` event on visibility change if labels changed on sidebar widget', async () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone, isVisible: true } });
+ const updatedLabel = {
+ id: 28,
+ title: 'Bug',
+ description: 'Label for bugs',
+ color: '#FF0000',
+ textColor: '#FFFFFF',
+ };
+ findLabelsView().vm.$emit('input', [updatedLabel]);
+ wrapper.setProps({ isVisible: false });
+ await nextTick();
+
+ expect(wrapper.emitted('setLabels')).toEqual([[[updatedLabel]]]);
+ });
+
+ it('does not render header on standalone variant', () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone } });
+
+ expect(findDropdownHeader().exists()).toBe(false);
+ });
+
+ it('renders header on embedded variant', () => {
+ createComponent({ props: { variant: DropdownVariant.Embedded } });
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+
+ it('renders header on sidebar variant', () => {
+ createComponent();
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+
describe('Create view', () => {
beforeEach(() => {
- wrapper.vm.toggleDropdownContentsCreateView();
+ createComponent({ data: { showDropdownContentsCreateView: true } });
});
it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => {
- expect(wrapper.findComponent(DropdownContentsCreateView).exists()).toBe(true);
+ expect(findCreateView().exists()).toBe(true);
});
it('does not render footer', () => {
@@ -67,11 +156,31 @@ describe('DropdownContent', () => {
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'));
+ await nextTick();
+
+ expect(findCreateView().exists()).toBe(false);
+ expect(findLabelsView().exists()).toBe(true);
+ });
+
+ it('changes the view to Labels view on `hideCreateView` event', async () => {
+ findCreateView().vm.$emit('hideCreateView');
+ await nextTick();
+
+ expect(findCreateView().exists()).toBe(false);
+ expect(findLabelsView().exists()).toBe(true);
+ });
});
describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => {
- expect(wrapper.findComponent(DropdownContentsLabelsView).exists()).toBe(true);
+ expect(findLabelsView().exists()).toBe(true);
});
it('renders footer on sidebar dropdown', () => {
@@ -109,19 +218,12 @@ describe('DropdownContent', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
- it('triggers `toggleDropdownContent` method on create label button click', () => {
- jest.spyOn(wrapper.vm, 'toggleDropdownContent').mockImplementation(() => {});
+ it('changes the view to Create on create label button click', async () => {
findCreateLabelButton().trigger('click');
- expect(wrapper.vm.toggleDropdownContent).toHaveBeenCalled();
+ await nextTick();
+ expect(findLabelsView().exists()).toBe(false);
});
});
});
-
- describe('template', () => {
- it('renders component container element with classes `gl-w-full gl-mt-2` and no styles', () => {
- expect(wrapper.attributes('class')).toContain('gl-w-full gl-mt-2');
- expect(wrapper.attributes('style')).toBeUndefined();
- });
- });
});
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 a18511fa21d..b5441d711a5 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
@@ -1,28 +1,55 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } 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 from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
-import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
+import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import { mockConfig, issuableLabelsQueryResponse } from './mock_data';
-import { mockConfig } from './mock_data';
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse);
+const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
describe('LabelsSelectRoot', () => {
let wrapper;
- const createComponent = (config = mockConfig, slots = {}) => {
+ const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findDropdownValue = () => wrapper.findComponent(DropdownValue);
+ const findDropdownContents = () => wrapper.findComponent(DropdownContents);
+
+ const createComponent = ({
+ config = mockConfig,
+ slots = {},
+ queryHandler = successfulQueryHandler,
+ } = {}) => {
+ const mockApollo = createMockApollo([[issueLabelsQuery, queryHandler]]);
+
wrapper = shallowMount(LabelsSelectRoot, {
slots,
- propsData: config,
+ apolloProvider: mockApollo,
+ localVue,
+ propsData: {
+ ...config,
+ issuableType: IssuableType.Issue,
+ },
stubs: {
- DropdownContents,
SidebarEditableItem,
},
provide: {
- iid: '1',
- projectPath: 'test',
canUpdate: true,
allowLabelEdit: true,
+ allowLabelCreate: true,
+ labelsManagePath: 'test',
},
});
};
@@ -42,33 +69,63 @@ describe('LabelsSelectRoot', () => {
${'embedded'} | ${'is-embedded'}
`(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
- ({ variant, cssClass }) => {
+ async ({ variant, cssClass }) => {
createComponent({
- ...mockConfig,
- variant,
+ config: { ...mockConfig, variant },
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain(cssClass);
- });
+ await nextTick();
+ expect(wrapper.classes()).toContain(cssClass);
},
);
- it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
- createComponent();
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
- });
+ describe('if dropdown variant is `sidebar`', () => {
+ it('renders sidebar editable item', () => {
+ createComponent();
+ expect(findSidebarEditableItem().exists()).toBe(true);
+ });
+
+ it('passes true `loading` prop to sidebar editable item when loading labels', () => {
+ createComponent();
+ expect(findSidebarEditableItem().props('loading')).toBe(true);
+ });
- it('renders `dropdown-value` component', async () => {
- createComponent(mockConfig, {
- default: 'None',
+ describe('when labels are fetched successfully', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('passes true `loading` prop to sidebar editable item', () => {
+ expect(findSidebarEditableItem().props('loading')).toBe(false);
+ });
+
+ it('renders dropdown value component when query labels is resolved', () => {
+ expect(findDropdownValue().exists()).toBe(true);
+ expect(findDropdownValue().props('selectedLabels')).toEqual(
+ issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes,
+ );
+ });
+
+ it('emits `onLabelRemove` event on dropdown value label remove event', () => {
+ const label = { id: 'gid://gitlab/ProjectLabel/1' };
+ findDropdownValue().vm.$emit('onLabelRemove', label);
+ expect(wrapper.emitted('onLabelRemove')).toEqual([[label]]);
+ });
});
- await wrapper.vm.$nextTick;
- const valueComp = wrapper.find(DropdownValue);
+ it('creates flash with error message when query is rejected', async () => {
+ createComponent({ queryHandler: errorQueryHandler });
+ await waitForPromises();
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
+ });
+ });
+
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => {
+ const label = { id: 'gid://gitlab/ProjectLabel/1' };
+ createComponent();
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
+ findDropdownContents().vm.$emit('setLabels', [label]);
+ expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[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 fceaabec2d0..23a457848d9 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
@@ -34,6 +34,8 @@ export const mockLabels = [
];
export const mockConfig = {
+ iid: '1',
+ fullPath: 'test',
allowMultiselect: true,
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
@@ -86,7 +88,7 @@ export const createLabelSuccessfulResponse = {
},
};
-export const labelsQueryResponse = {
+export const workspaceLabelsQueryResponse = {
data: {
workspace: {
labels: {
@@ -108,3 +110,23 @@ export const labelsQueryResponse = {
},
},
};
+
+export const issuableLabelsQueryResponse = {
+ data: {
+ workspace: {
+ issuable: {
+ id: '1',
+ labels: {
+ nodes: [
+ {
+ color: '#330066',
+ description: null,
+ id: 'gid://gitlab/ProjectLabel/1',
+ title: 'Label1',
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
index af4fa462cbf..0f1e118d44c 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -45,6 +45,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
>
<div
class="mw-50 gl-text-center"
+ style="display: none;"
>
<h3
class=""
@@ -61,7 +62,6 @@ exports[`Upload dropzone component correctly overrides description and drop mess
<div
class="mw-50 gl-text-center"
- style="display: none;"
>
<h3
class=""
@@ -146,7 +146,6 @@ exports[`Upload dropzone component when dragging renders correct template when d
<div
class="mw-50 gl-text-center"
- style=""
>
<h3
class=""
@@ -231,7 +230,6 @@ exports[`Upload dropzone component when dragging renders correct template when d
<div
class="mw-50 gl-text-center"
- style=""
>
<h3
class=""
@@ -299,6 +297,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<div
class="mw-50 gl-text-center"
+ style=""
>
<h3
class=""
@@ -383,6 +382,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<div
class="mw-50 gl-text-center"
+ style=""
>
<h3
class=""
@@ -467,6 +467,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
>
<div
class="mw-50 gl-text-center"
+ style=""
>
<h3
class=""
@@ -551,6 +552,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
>
<div
class="mw-50 gl-text-center"
+ style="display: none;"
>
<h3
class=""
@@ -567,7 +569,6 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
<div
class="mw-50 gl-text-center"
- style="display: none;"
>
<h3
class=""
@@ -603,6 +604,7 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot
>
<div
class="mw-50 gl-text-center"
+ style="display: none;"
>
<h3
class=""
@@ -619,7 +621,6 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot
<div
class="mw-50 gl-text-center"
- style="display: none;"
>
<h3
class=""
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
new file mode 100644
index 00000000000..a92f058f311
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -0,0 +1,116 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+
+const mockSchedules = [
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'Schedule 1',
+ url: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
+ projectName: 'Shell',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
+ },
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'Schedule 2',
+ url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
+ projectName: 'UI',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
+ },
+];
+const mockPolicies = [
+ {
+ type: OBSTACLE_TYPES.escalationPolicies,
+ name: 'Policy 1',
+ url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/escalation-policies',
+ projectName: 'UI',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
+ },
+];
+const mockObstacles = mockSchedules.concat(mockPolicies);
+
+const userName = "O'User";
+
+describe('User deletion obstacles list', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = extendedWrapper(
+ shallowMount(UserDeletionObstaclesList, {
+ propsData: {
+ obstacles: mockObstacles,
+ userName,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ }),
+ );
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findFooter = () => wrapper.findByTestId('footer');
+ const findObstacles = () => wrapper.findByTestId('obstacles-list');
+
+ describe.each`
+ isCurrentUser | titleText | footerText
+ ${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
+ ${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
+ `('when current user', ({ isCurrentUser, titleText, footerText }) => {
+ it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, async () => {
+ createComponent({
+ isCurrentUser,
+ });
+
+ expect(findTitle().text()).toBe(titleText);
+ expect(findFooter().text()).toBe(footerText);
+ });
+ });
+
+ describe.each(mockObstacles)(
+ 'renders all obstacles',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the project name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`in Project ${projectName}`);
+ expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
+ });
+ },
+ );
+
+ describe.each(mockSchedules)(
+ 'renders on-call schedules',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the schedule name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`On-call schedule ${name}`);
+ expect(findLinks().at(0).attributes('href')).toBe(url);
+ });
+ },
+ );
+
+ describe.each(mockPolicies)(
+ 'renders escalation policies',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the policy name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`Escalation policy ${name}`);
+ expect(findLinks().at(0).attributes('href')).toBe(url);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js
new file mode 100644
index 00000000000..99f739098f7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js
@@ -0,0 +1,43 @@
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
+
+describe('parseUserDeletionObstacles', () => {
+ const mockObstacles = [{ name: 'Obstacle' }];
+ const expectedSchedule = { name: 'Obstacle', type: OBSTACLE_TYPES.oncallSchedules };
+ const expectedPolicy = { name: 'Obstacle', type: OBSTACLE_TYPES.escalationPolicies };
+
+ it('is undefined when user is not available', () => {
+ expect(parseUserDeletionObstacles()).toHaveLength(0);
+ });
+
+ it('is empty when obstacles are not available for user', () => {
+ expect(parseUserDeletionObstacles({})).toHaveLength(0);
+ });
+
+ it('is empty when user has no obstacles to deletion', () => {
+ const input = { oncallSchedules: [], escalationPolicies: [] };
+
+ expect(parseUserDeletionObstacles(input)).toHaveLength(0);
+ });
+
+ it('returns obstacles with type when user is part of on-call schedules', () => {
+ const input = { oncallSchedules: mockObstacles, escalationPolicies: [] };
+ const expectedOutput = [expectedSchedule];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+
+ it('returns obstacles with type when user is part of escalation policies', () => {
+ const input = { oncallSchedules: [], escalationPolicies: mockObstacles };
+ const expectedOutput = [expectedPolicy];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+
+ it('returns obstacles with type when user have every obstacle type', () => {
+ const input = { oncallSchedules: mockObstacles, escalationPolicies: mockObstacles };
+ const expectedOutput = [expectedSchedule, expectedPolicy];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 926223e0670..09633daf587 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -9,6 +9,7 @@ const DEFAULT_PROPS = {
username: 'root',
name: 'Administrator',
location: 'Vienna',
+ localTime: '2:30 PM',
bot: false,
bio: null,
workInformation: null,
@@ -31,10 +32,11 @@ describe('User Popover Component', () => {
wrapper.destroy();
});
- const findUserStatus = () => wrapper.find('.js-user-status');
+ const findUserStatus = () => wrapper.findByTestId('user-popover-status');
const findTarget = () => document.querySelector('.js-user-link');
const findUserName = () => wrapper.find(UserNameWithStatus);
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
+ const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
const createWrapper = (props = {}, options = {}) => {
wrapper = mountExtended(UserPopover, {
@@ -71,7 +73,6 @@ describe('User Popover Component', () => {
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
- expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location);
});
it('shows icon for location', () => {
@@ -164,6 +165,25 @@ describe('User Popover Component', () => {
});
});
+ describe('local time', () => {
+ it('should show local time when it is available', () => {
+ createWrapper();
+
+ expect(findUserLocalTime().exists()).toBe(true);
+ });
+
+ it('should not show local time when it is not available', () => {
+ const user = {
+ ...DEFAULT_PROPS.user,
+ localTime: null,
+ };
+
+ createWrapper({ user });
+
+ expect(findUserLocalTime().exists()).toBe(false);
+ });
+ });
+
describe('status data', () => {
it('should show only message', () => {
const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } };
@@ -256,5 +276,11 @@ describe('User Popover Component', () => {
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"');
});
+
+ it('does not display local time', () => {
+ createWrapper({ user: SECURITY_BOT_USER });
+
+ expect(findUserLocalTime().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 5fe4eeb6061..92938b2717f 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -160,4 +160,26 @@ describe('Web IDE link component', () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_GITPOD.key);
});
});
+
+ describe('edit actions', () => {
+ it.each([
+ {
+ props: { showWebIdeButton: true, showEditButton: false },
+ expectedEventPayload: 'ide',
+ },
+ {
+ props: { showWebIdeButton: false, showEditButton: true },
+ expectedEventPayload: 'simple',
+ },
+ ])(
+ 'emits the correct event when an action handler is called',
+ async ({ props, expectedEventPayload }) => {
+ createComponent({ ...props, needsToFork: true });
+
+ findActionsButton().props('actions')[0].handle();
+
+ expect(wrapper.emitted('edit')).toEqual([[expectedEventPayload]]);
+ },
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/directives/validation_spec.js b/spec/frontend/vue_shared/directives/validation_spec.js
index 51ee73cabde..dcd3a44a6fc 100644
--- a/spec/frontend/vue_shared/directives/validation_spec.js
+++ b/spec/frontend/vue_shared/directives/validation_spec.js
@@ -4,11 +4,13 @@ import validation, { initForm } from '~/vue_shared/directives/validation';
describe('validation directive', () => {
let wrapper;
- const createComponentFactory = ({ inputAttributes, template, data }) => {
- const defaultInputAttributes = {
- type: 'text',
- required: true,
- };
+ const createComponentFactory = (options) => {
+ const {
+ inputAttributes = { type: 'text', required: true },
+ template,
+ data,
+ feedbackMap = {},
+ } = options;
const defaultTemplate = `
<form>
@@ -18,11 +20,11 @@ describe('validation directive', () => {
const component = {
directives: {
- validation: validation(),
+ validation: validation(feedbackMap),
},
data() {
return {
- attributes: inputAttributes || defaultInputAttributes,
+ attributes: inputAttributes,
...data,
};
},
@@ -32,8 +34,10 @@ describe('validation directive', () => {
wrapper = shallowMount(component, { attachTo: document.body });
};
- const createComponent = ({ inputAttributes, showValidation, template } = {}) =>
- createComponentFactory({
+ const createComponent = (options = {}) => {
+ const { inputAttributes, showValidation, template, feedbackMap } = options;
+
+ return createComponentFactory({
inputAttributes,
data: {
showValidation,
@@ -48,10 +52,14 @@ describe('validation directive', () => {
},
},
template,
+ feedbackMap,
});
+ };
+
+ const createComponentWithInitForm = (options = {}) => {
+ const { inputAttributes, feedbackMap } = options;
- const createComponentWithInitForm = ({ inputAttributes } = {}) =>
- createComponentFactory({
+ return createComponentFactory({
inputAttributes,
data: {
form: initForm({
@@ -68,7 +76,9 @@ describe('validation directive', () => {
<input v-validation:[form.showValidation] name="exampleField" v-bind="attributes" />
</form>
`,
+ feedbackMap,
});
+ };
afterEach(() => {
wrapper.destroy();
@@ -209,6 +219,111 @@ describe('validation directive', () => {
});
});
+ describe('with custom feedbackMap', () => {
+ const customMessage = 'Please fill out the name field.';
+ const template = `
+ <form>
+ <div v-validation:[showValidation]>
+ <input name="exampleField" v-bind="attributes" />
+ </div>
+ </form>
+ `;
+ beforeEach(() => {
+ const feedbackMap = {
+ valueMissing: {
+ isInvalid: (el) => el.validity?.valueMissing,
+ message: customMessage,
+ },
+ };
+
+ createComponent({
+ template,
+ inputAttributes: {
+ required: true,
+ },
+ feedbackMap,
+ });
+ });
+
+ describe('with invalid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('');
+ });
+
+ it('should set correct field state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: false,
+ feedback: customMessage,
+ });
+ });
+ });
+
+ describe('with valid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('hello');
+ });
+
+ it('set the correct state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: true,
+ feedback: '',
+ });
+ });
+ });
+ });
+
+ describe('with validation-message present on the element', () => {
+ const customMessage = 'The name field is required.';
+ const template = `
+ <form>
+ <div v-validation:[showValidation]>
+ <input name="exampleField" v-bind="attributes" validation-message="${customMessage}" />
+ </div>
+ </form>
+ `;
+ beforeEach(() => {
+ const feedbackMap = {
+ valueMissing: {
+ isInvalid: (el) => el.validity?.valueMissing,
+ },
+ };
+
+ createComponent({
+ template,
+ inputAttributes: {
+ required: true,
+ },
+ feedbackMap,
+ });
+ });
+
+ describe('with invalid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('');
+ });
+
+ it('should set correct field state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: false,
+ feedback: customMessage,
+ });
+ });
+ });
+
+ describe('with valid value', () => {
+ beforeEach(() => {
+ setValueAndTriggerValidation('hello');
+ });
+
+ it('set the correct state', () => {
+ expect(getFormData().fields.exampleField).toEqual({
+ state: true,
+ feedback: '',
+ });
+ });
+ });
+ });
+
describe('component using initForm', () => {
it('sets the form fields correctly', () => {
createComponentWithInitForm();
diff --git a/spec/frontend/vue_shared/oncall_schedules_list_spec.js b/spec/frontend/vue_shared/oncall_schedules_list_spec.js
deleted file mode 100644
index f83a5187b8b..00000000000
--- a/spec/frontend/vue_shared/oncall_schedules_list_spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
-
-const mockSchedules = [
- {
- name: 'Schedule 1',
- scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
- projectName: 'Shell',
- projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
- },
- {
- name: 'Schedule 2',
- scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
- projectName: 'UI',
- projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
- },
-];
-
-const userName = "O'User";
-
-describe('On-call schedules list', () => {
- let wrapper;
-
- function createComponent(props) {
- wrapper = extendedWrapper(
- shallowMount(OncallSchedulesList, {
- propsData: {
- schedules: mockSchedules,
- userName,
- ...props,
- },
- stubs: {
- GlSprintf,
- },
- }),
- );
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findLinks = () => wrapper.findAllComponents(GlLink);
- const findTitle = () => wrapper.findByTestId('title');
- const findFooter = () => wrapper.findByTestId('footer');
- const findSchedules = () => wrapper.findByTestId('schedules-list');
-
- describe.each`
- isCurrentUser | titleText | footerText
- ${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
- ${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
- `('when current user ', ({ isCurrentUser, titleText, footerText }) => {
- it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call schedule`, async () => {
- createComponent({
- isCurrentUser,
- });
-
- expect(findTitle().text()).toBe(titleText);
- expect(findFooter().text()).toBe(footerText);
- });
- });
-
- describe.each(mockSchedules)(
- 'renders each on-call schedule data',
- ({ name, scheduleUrl, projectName, projectUrl }) => {
- beforeEach(() => {
- createComponent({ schedules: [{ name, scheduleUrl, projectName, projectUrl }] });
- });
-
- it(`renders schedule ${name}'s name and link`, () => {
- const msg = findSchedules().text();
-
- expect(msg).toContain(`On-call schedule ${name}`);
- expect(findLinks().at(0).attributes('href')).toBe(scheduleUrl);
- });
-
- it(`renders project ${projectName}'s name and link`, () => {
- const msg = findSchedules().text();
-
- expect(msg).toContain(`in Project ${projectName}`);
- expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
- });
- },
- );
-});
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index 06631710509..cdaeec78e47 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -314,7 +314,7 @@ export const sastDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
-export const secretScanningDiffSuccessMock = {
+export const secretDetectionDiffSuccessMock = {
added: [mockFindings[0], mockFindings[1]],
fixed: [mockFindings[2]],
base_report_created_at: '2020-01-01T10:00:00.000Z',
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 4d579fa61df..68a97103d3a 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -12,7 +12,7 @@ import {
securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse,
securityReportMergeRequestDownloadPathsQueryResponse,
sastDiffSuccessMock,
- secretScanningDiffSuccessMock,
+ secretDetectionDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -31,7 +31,7 @@ Vue.use(VueApollo);
Vue.use(Vuex);
const SAST_COMPARISON_PATH = '/sast.json';
-const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
+const SECRET_DETECTION_COMPARISON_PATH = '/secret_detection.json';
describe('Security reports app', () => {
let wrapper;
@@ -175,12 +175,12 @@ describe('Security reports app', () => {
const SAST_SUCCESS_MESSAGE =
'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
- const SECRET_SCANNING_SUCCESS_MESSAGE =
+ const SECRET_DETECTION_SUCCESS_MESSAGE =
'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
describe.each`
- reportType | pathProp | path | successResponse | successMessage
- ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
- ${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
+ reportType | pathProp | path | successResponse | successMessage
+ ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
+ ${REPORT_TYPE_SECRET_DETECTION} | ${'secretDetectionComparisonPath'} | ${SECRET_DETECTION_COMPARISON_PATH} | ${secretDetectionDiffSuccessMock} | ${SECRET_DETECTION_SUCCESS_MESSAGE}
`(
'given a $pathProp and $reportType artifact',
({ pathProp, path, successResponse, successMessage }) => {
diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
index 97746c7c38b..bcc8955ba02 100644
--- a/spec/frontend/vue_shared/security_reports/store/getters_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
@@ -8,7 +8,7 @@ import {
summaryCounts,
} from '~/vue_shared/security_reports/store/getters';
import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
-import createSecretScanningState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
+import createSecretDetectionState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
import createState from '~/vue_shared/security_reports/store/state';
import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
@@ -21,7 +21,7 @@ describe('Security reports getters', () => {
beforeEach(() => {
state = createState();
state.sast = createSastState();
- state.secretDetection = createSecretScanningState();
+ state.secretDetection = createSecretDetectionState();
});
describe('summaryCounts', () => {