summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 14:22:11 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-20 14:22:11 +0000
commit0c872e02b2c822e3397515ec324051ff540f0cd5 (patch)
treece2fb6ce7030e4dad0f4118d21ab6453e5938cdd /spec/frontend
parentf7e05a6853b12f02911494c4b3fe53d9540d74fc (diff)
downloadgitlab-ce-0c872e02b2c822e3397515ec324051ff540f0cd5.tar.gz
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/dom_events_helper.js8
-rw-r--r--spec/frontend/__helpers__/filtered_search_spec_helper.js4
-rw-r--r--spec/frontend/__helpers__/graphql_helpers.js14
-rw-r--r--spec/frontend/__helpers__/graphql_helpers_spec.js23
-rw-r--r--spec/frontend/__helpers__/graphql_transformer.js4
-rw-r--r--spec/frontend/__helpers__/jest_helpers.js22
-rw-r--r--spec/frontend/__helpers__/mock_window_location_helper.js15
-rw-r--r--spec/frontend/__helpers__/raw_transformer.js2
-rw-r--r--spec/frontend/__helpers__/set_timeout_promise_helper.js4
-rw-r--r--spec/frontend/__helpers__/web_worker_transformer.js10
-rw-r--r--spec/frontend/__helpers__/yaml_transformer.js2
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js2
-rw-r--r--spec/frontend/admin/background_migrations/components/database_listbox_spec.js10
-rw-r--r--spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js46
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js201
-rw-r--r--spec/frontend/admin/broadcast_messages/mock_data.js8
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js1
-rw-r--r--spec/frontend/admin/signup_restrictions/mock_data.js2
-rw-r--r--spec/frontend/admin/signup_restrictions/utils_spec.js1
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap (renamed from spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap)0
-rw-r--r--spec/frontend/analytics/cycle_analytics/base_spec.js (renamed from spec/frontend/cycle_analytics/base_spec.js)12
-rw-r--r--spec/frontend/analytics/cycle_analytics/filter_bar_spec.js (renamed from spec/frontend/cycle_analytics/filter_bar_spec.js)22
-rw-r--r--spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js (renamed from spec/frontend/cycle_analytics/formatted_stage_count_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js (renamed from spec/frontend/cycle_analytics/mock_data.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/path_navigation_spec.js (renamed from spec/frontend/cycle_analytics/path_navigation_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/stage_table_spec.js (renamed from spec/frontend/cycle_analytics/stage_table_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/actions_spec.js (renamed from spec/frontend/cycle_analytics/store/actions_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/getters_spec.js (renamed from spec/frontend/cycle_analytics/store/getters_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/mutations_spec.js (renamed from spec/frontend/cycle_analytics/store/mutations_spec.js)6
-rw-r--r--spec/frontend/analytics/cycle_analytics/total_time_spec.js (renamed from spec/frontend/cycle_analytics/total_time_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/utils_spec.js (renamed from spec/frontend/cycle_analytics/utils_spec.js)2
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js (renamed from spec/frontend/cycle_analytics/value_stream_filters_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js (renamed from spec/frontend/cycle_analytics/value_stream_metrics_spec.js)0
-rw-r--r--spec/frontend/api_spec.js20
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js8
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js3
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js3
-rw-r--r--spec/frontend/behaviors/markdown/render_observability_spec.js38
-rw-r--r--spec/frontend/blob/openapi/index_spec.js2
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js24
-rw-r--r--spec/frontend/boards/board_list_spec.js11
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js16
-rw-r--r--spec/frontend/boards/components/board_content_spec.js28
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js16
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js8
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js15
-rw-r--r--spec/frontend/boards/mock_data.js40
-rw-r--r--spec/frontend/boards/project_select_spec.js2
-rw-r--r--spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js11
-rw-r--r--spec/frontend/ci/ci_lint/components/ci_lint_spec.js (renamed from spec/frontend/ci_lint/components/ci_lint_spec.js)6
-rw-r--r--spec/frontend/ci/ci_lint/mock_data.js (renamed from spec/frontend/ci_lint/mock_data.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js (renamed from spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js (renamed from spec/frontend/pipeline_editor/components/commit/commit_form_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js (renamed from spec/frontend/pipeline_editor/components/commit/commit_section_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js (renamed from spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js (renamed from spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js (renamed from spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js (renamed from spec/frontend/pipeline_editor/components/editor/text_editor_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js (renamed from spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js)12
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js (renamed from spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js)10
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js (renamed from spec/frontend/pipeline_editor/components/file-tree/container_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js (renamed from spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js (renamed from spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js (renamed from spec/frontend/pipeline_editor/components/header/validation_segment_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js (renamed from spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js (renamed from spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js (renamed from spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js (renamed from spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js (renamed from spec/frontend/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js (renamed from spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_messages_spec.js (renamed from spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js (renamed from spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js)12
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap (renamed from spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap)2
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js (renamed from spec/frontend/pipeline_editor/graphql/resolvers_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js (renamed from spec/frontend/pipeline_editor/mock_data.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js (renamed from spec/frontend/pipeline_editor/pipeline_editor_app_spec.js)34
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js (renamed from spec/frontend/pipeline_editor/pipeline_editor_home_spec.js)20
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js149
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js (renamed from spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js)4
-rw-r--r--spec/frontend/ci/reports/codequality_report/mock_data.js (renamed from spec/frontend/reports/codequality_report/mock_data.js)0
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/actions_spec.js (renamed from spec/frontend/reports/codequality_report/store/actions_spec.js)8
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/getters_spec.js (renamed from spec/frontend/reports/codequality_report/store/getters_spec.js)6
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/mutations_spec.js (renamed from spec/frontend/reports/codequality_report/store/mutations_spec.js)6
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js (renamed from spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap (renamed from spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap)0
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap (renamed from spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap)0
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js (renamed from spec/frontend/reports/components/grouped_issues_list_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/issue_status_icon_spec.js (renamed from spec/frontend/reports/components/issue_status_icon_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/report_item_spec.js (renamed from spec/frontend/reports/components/report_item_spec.js)8
-rw-r--r--spec/frontend/ci/reports/components/report_link_spec.js (renamed from spec/frontend/reports/components/report_link_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/report_section_spec.js (renamed from spec/frontend/reports/components/report_section_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js (renamed from spec/frontend/reports/components/summary_row_spec.js)2
-rw-r--r--spec/frontend/ci/reports/mock_data/mock_data.js (renamed from spec/frontend/reports/mock_data/mock_data.js)0
-rw-r--r--spec/frontend/ci/reports/mock_data/new_and_fixed_failures_report.json (renamed from spec/frontend/reports/mock_data/new_and_fixed_failures_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/new_errors_report.json (renamed from spec/frontend/reports/mock_data/new_errors_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/new_failures_report.json (renamed from spec/frontend/reports/mock_data/new_failures_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/new_failures_with_null_files_report.json (renamed from spec/frontend/reports/mock_data/new_failures_with_null_files_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/no_failures_report.json (renamed from spec/frontend/reports/mock_data/no_failures_report.json)23
-rw-r--r--spec/frontend/ci/reports/mock_data/recent_failures_report.json (renamed from spec/frontend/reports/mock_data/recent_failures_report.json)26
-rw-r--r--spec/frontend/ci/reports/mock_data/resolved_failures.json (renamed from spec/frontend/reports/mock_data/resolved_failures.json)25
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js51
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js37
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js (renamed from spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js)29
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js3
-rw-r--r--spec/frontend/ci/runner/components/runner_job_status_badge_spec.js51
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js15
-rw-r--r--spec/frontend/ci/runner/components/runner_status_badge_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_stats_spec.js53
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js16
-rw-r--r--spec/frontend/ci/runner/mock_data.js7
-rw-r--r--spec/frontend/ci/runner/runner_search_utils_spec.js16
-rw-r--r--spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js1
-rw-r--r--spec/frontend/ci_variable_list/components/ci_group_variables_spec.js1
-rw-r--r--spec/frontend/ci_variable_list/components/ci_project_variables_spec.js1
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js75
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js27
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js38
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_table_spec.js80
-rw-r--r--spec/frontend/ci_variable_list/mocks.js16
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js40
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js39
-rw-r--r--spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js95
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js4
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js4
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap8
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js19
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js (renamed from spec/frontend/content_editor/components/top_toolbar_spec.js)4
-rw-r--r--spec/frontend/content_editor/extensions/comment_spec.js30
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js12
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js53
-rw-r--r--spec/frontend/content_editor/test_utils.js4
-rw-r--r--spec/frontend/crm/contact_form_wrapper_spec.js18
-rw-r--r--spec/frontend/crm/crm_form_spec.js (renamed from spec/frontend/crm/form_spec.js)4
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js88
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js72
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js2
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap342
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js19
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js15
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js36
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js3
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js28
-rw-r--r--spec/frontend/diffs/store/actions_spec.js44
-rw-r--r--spec/frontend/diffs/utils/merge_request_spec.js40
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js73
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js14
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml13
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml31
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml11
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml39
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml4
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml17
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml11
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml28
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml9
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js14
-rw-r--r--spec/frontend/environment.js11
-rw-r--r--spec/frontend/environments/environment_details_page_spec.js50
-rw-r--r--spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap127
-rw-r--r--spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js96
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js39
-rw-r--r--spec/frontend/feature_flags/components/strategy_label_spec.js61
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js6
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js3
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js6
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js5
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js4
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb8
-rw-r--r--spec/frontend/fixtures/api_projects.rb8
-rw-r--r--spec/frontend/fixtures/environments.rb53
-rw-r--r--spec/frontend/fixtures/freeze_period.rb9
-rw-r--r--spec/frontend/fixtures/releases.rb18
-rw-r--r--spec/frontend/fixtures/runner_instructions.rb43
-rw-r--r--spec/frontend/fixtures/tabs.rb8
-rw-r--r--spec/frontend/flash_spec.js24
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js3
-rw-r--r--spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js202
-rw-r--r--spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js84
-rw-r--r--spec/frontend/gitlab_version_check/index_spec.js138
-rw-r--r--spec/frontend/gitlab_version_check/mock_data.js22
-rw-r--r--spec/frontend/gitlab_version_check/utils_spec.js35
-rw-r--r--spec/frontend/groups/components/app_spec.js66
-rw-r--r--spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js27
-rw-r--r--spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js27
-rw-r--r--spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js (renamed from spec/frontend/groups/components/empty_state_spec.js)18
-rw-r--r--spec/frontend/groups/components/group_name_and_path_spec.js2
-rw-r--r--spec/frontend/groups/components/groups_spec.js10
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js33
-rw-r--r--spec/frontend/header_search/components/app_spec.js1
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js33
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js94
-rw-r--r--spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js214
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js164
-rw-r--r--spec/frontend/ide/lib/common/model_spec.js9
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js22
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js32
-rw-r--r--spec/frontend/ide/remote/index_spec.js91
-rw-r--r--spec/frontend/ide/services/index_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/messages_spec.js4
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js76
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js38
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js44
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js28
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js39
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js66
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js64
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js230
-rw-r--r--spec/frontend/import_entities/import_projects/store/getters_spec.js19
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js79
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js26
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_actions_spec.js227
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js420
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js46
-rw-r--r--spec/frontend/invite_members/components/invite_group_notification_spec.js42
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js63
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js81
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js86
-rw-r--r--spec/frontend/invite_members/mock_data/group_modal.js2
-rw-r--r--spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js54
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js332
-rw-r--r--spec/frontend/issues/dashboard/mock_data.js88
-rw-r--r--spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js68
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js211
-rw-r--r--spec/frontend/issues/list/components/issue_card_statistics_spec.js64
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js192
-rw-r--r--spec/frontend/issues/list/mock_data.js40
-rw-r--r--spec/frontend/issues/list/utils_spec.js28
-rw-r--r--spec/frontend/issues/related_merge_requests/store/actions_spec.js6
-rw-r--r--spec/frontend/issues/show/components/app_spec.js30
-rw-r--r--spec/frontend/issues/show/components/description_spec.js10
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js34
-rw-r--r--spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js8
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js83
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js21
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js41
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js27
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js17
-rw-r--r--spec/frontend/issues/show/components/locked_warning_spec.js55
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js82
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js56
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js35
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap20
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js14
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js8
-rw-r--r--spec/frontend/jobs/components/job/empty_state_spec.js19
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js175
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js1
-rw-r--r--spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js156
-rw-r--r--spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js109
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js232
-rw-r--r--spec/frontend/jobs/components/job/mock_data.js76
-rw-r--r--spec/frontend/jobs/components/job/sidebar_header_spec.js128
-rw-r--r--spec/frontend/jobs/mock_data.js6
-rw-r--r--spec/frontend/language_switcher/components/app_spec.js62
-rw-r--r--spec/frontend/language_switcher/mock_data.js26
-rw-r--r--spec/frontend/lib/dompurify_spec.js5
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js32
-rw-r--r--spec/frontend/lib/utils/create_and_submit_form_spec.js73
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js18
-rw-r--r--spec/frontend/lib/utils/poll_until_complete_spec.js4
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js13
-rw-r--r--spec/frontend/listbox/index_spec.js4
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js13
-rw-r--r--spec/frontend/merge_request_tabs_spec.js1
-rw-r--r--spec/frontend/merge_requests/components/target_project_dropdown_spec.js80
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js4
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap233
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap (renamed from spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap)73
-rw-r--r--spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js2
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js43
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js (renamed from spec/frontend/ml/experiment_tracking/components/experiment_spec.js)12
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap4
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js14
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js19
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js11
-rw-r--r--spec/frontend/monitoring/utils_spec.js1
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js98
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js3
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js3
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js110
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js53
-rw-r--r--spec/frontend/notes/stores/actions_spec.js67
-rw-r--r--spec/frontend/observability/observability_app_spec.js186
-rw-r--r--spec/frontend/observability/skeleton_spec.js96
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js9
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js35
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js31
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/shared/utils_spec.js2
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js117
-rw-r--r--spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js6
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js172
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap88
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js24
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js116
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js211
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_content_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js13
-rw-r--r--spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js456
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js4
-rw-r--r--spec/frontend/pipeline_new/mock_data.js4
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js12
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js19
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js15
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js23
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js7
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js8
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js3
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js38
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js32
-rw-r--r--spec/frontend/projects/project_new_spec.js55
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js15
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js4
-rw-r--r--spec/frontend/projects/settings/mock_data.js57
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js7
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js20
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js68
-rw-r--r--spec/frontend/projects/settings/utils_spec.js11
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap22
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js231
-rw-r--r--spec/frontend/releases/components/release_block_spec.js7
-rw-r--r--spec/frontend/repository/components/table/index_spec.js3
-rw-r--r--spec/frontend/repository/components/table/row_spec.js3
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js1
-rw-r--r--spec/frontend/repository/utils/ref_switcher_utils_spec.js22
-rw-r--r--spec/frontend/search/mock_data.js1
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js63
-rw-r--r--spec/frontend/search/sidebar/components/filters_spec.js40
-rw-r--r--spec/frontend/search/sidebar/components/scope_navigation_spec.js41
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js63
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js76
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js6
-rw-r--r--spec/frontend/sentry/index_spec.js60
-rw-r--r--spec/frontend/sentry/legacy_index_spec.js64
-rw-r--r--spec/frontend/sentry/legacy_sentry_config_spec.js215
-rw-r--r--spec/frontend/sentry/sentry_browser_wrapper_spec.js59
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js131
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_title_spec.js (renamed from spec/frontend/sidebar/assignee_title_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js (renamed from spec/frontend/sidebar/assignees_realtime_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js (renamed from spec/frontend/sidebar/assignees_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js (renamed from spec/frontend/sidebar/issuable_assignees_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js (renamed from spec/frontend/sidebar/sidebar_assignees_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js4
-rw-r--r--spec/frontend/sidebar/components/copy/copy_email_to_clipboard_spec.js (renamed from spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/copy/copyable_field_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js (renamed from spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js (renamed from spec/frontend/sidebar/components/crm_contacts_spec.js)6
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_status_spec.js6
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_utils_spec.js4
-rw-r--r--spec/frontend/sidebar/components/incidents/mock_data.js2
-rw-r--r--spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js83
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js)12
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js)6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js)18
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/mock_data.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js)0
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js)6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/getters_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/mutations_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js)8
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js)10
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js77
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js)60
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap (renamed from spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap)0
-rw-r--r--spec/frontend/sidebar/components/lock/constants.js (renamed from spec/frontend/sidebar/lock/constants.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js (renamed from spec/frontend/sidebar/lock/edit_form_buttons_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_spec.js (renamed from spec/frontend/sidebar/lock/edit_form_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js (renamed from spec/frontend/sidebar/lock/issuable_lock_form_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/move/move_issues_button_spec.js (renamed from spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js)26
-rw-r--r--spec/frontend/sidebar/components/participants/participants_spec.js (renamed from spec/frontend/sidebar/participants_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js (renamed from spec/frontend/sidebar/reviewer_title_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewers_spec.js (renamed from spec/frontend/sidebar/reviewers_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js77
-rw-r--r--spec/frontend/sidebar/components/severity/severity_spec.js2
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js4
-rw-r--r--spec/frontend/sidebar/components/status/status_dropdown_spec.js (renamed from spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js (renamed from spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js)4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js (renamed from spec/frontend/sidebar/subscriptions_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js219
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js6
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js63
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap (renamed from spec/frontend/sidebar/__snapshots__/todo_spec.js.snap)0
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js2
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/todo_button_spec.js)2
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_spec.js (renamed from spec/frontend/sidebar/todo_spec.js)0
-rw-r--r--spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js)2
-rw-r--r--spec/frontend/sidebar/lib/sidebar_move_issue_spec.js (renamed from spec/frontend/sidebar/sidebar_move_issue_spec.js)2
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js58
-rw-r--r--spec/frontend/sidebar/stores/sidebar_store_spec.js (renamed from spec/frontend/sidebar/sidebar_store_spec.js)2
-rw-r--r--spec/frontend/terms/components/app_spec.js7
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js21
-rw-r--r--spec/frontend/token_access/mock_data.js13
-rw-r--r--spec/frontend/token_access/token_access_spec.js109
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js141
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js28
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js47
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js38
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js20
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js55
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js25
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap305
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js49
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js109
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js (renamed from spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js)104
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js132
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json30
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/mock_data.js122
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js87
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js53
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js216
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js141
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js18
-rw-r--r--spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap4
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js111
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_description_rendered_spec.js8
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js34
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js91
-rw-r--r--spec/frontend/work_items/components/work_item_information_spec.js43
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js35
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js67
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js161
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js47
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js188
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js147
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js12
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js107
-rw-r--r--spec/frontend/work_items/mock_data.js588
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js2
-rw-r--r--spec/frontend/work_items/router_spec.js16
508 files changed, 12531 insertions, 5709 deletions
diff --git a/spec/frontend/__helpers__/dom_events_helper.js b/spec/frontend/__helpers__/dom_events_helper.js
deleted file mode 100644
index 865ea97903f..00000000000
--- a/spec/frontend/__helpers__/dom_events_helper.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const triggerDOMEvent = (type) => {
- window.document.dispatchEvent(
- new Event(type, {
- bubbles: true,
- cancelable: true,
- }),
- );
-};
diff --git a/spec/frontend/__helpers__/filtered_search_spec_helper.js b/spec/frontend/__helpers__/filtered_search_spec_helper.js
index ecf10694a16..f76fdfca229 100644
--- a/spec/frontend/__helpers__/filtered_search_spec_helper.js
+++ b/spec/frontend/__helpers__/filtered_search_spec_helper.js
@@ -1,3 +1,5 @@
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+
export default class FilteredSearchSpecHelper {
static createFilterVisualTokenHTML(name, operator, value, isSelected) {
return FilteredSearchSpecHelper.createFilterVisualToken(name, operator, value, isSelected)
@@ -43,7 +45,7 @@ export default class FilteredSearchSpecHelper {
static createSearchVisualToken(name) {
const li = document.createElement('li');
- li.classList.add('js-visual-token', 'filtered-search-term');
+ li.classList.add('js-visual-token', FILTERED_SEARCH_TERM);
li.innerHTML = `<div class="name">${name}</div>`;
return li;
}
diff --git a/spec/frontend/__helpers__/graphql_helpers.js b/spec/frontend/__helpers__/graphql_helpers.js
deleted file mode 100644
index 63123aa046f..00000000000
--- a/spec/frontend/__helpers__/graphql_helpers.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * Returns a clone of the given object with all __typename keys omitted,
- * including deeply nested ones.
- *
- * Only works with JSON-serializable objects.
- *
- * @param {object} An object with __typename keys (e.g., a GraphQL response)
- * @returns {object} A new object with no __typename keys
- */
-export const stripTypenames = (object) => {
- return JSON.parse(
- JSON.stringify(object, (key, value) => (key === '__typename' ? undefined : value)),
- );
-};
diff --git a/spec/frontend/__helpers__/graphql_helpers_spec.js b/spec/frontend/__helpers__/graphql_helpers_spec.js
deleted file mode 100644
index dd23fbbf4e9..00000000000
--- a/spec/frontend/__helpers__/graphql_helpers_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { stripTypenames } from './graphql_helpers';
-
-describe('stripTypenames', () => {
- it.each`
- input | expected
- ${{}} | ${{}}
- ${{ __typename: 'Foo' }} | ${{}}
- ${{ bar: 'bar', __typename: 'Foo' }} | ${{ bar: 'bar' }}
- ${{ bar: { __typename: 'Bar' }, __typename: 'Foo' }} | ${{ bar: {} }}
- ${{ bar: [{ __typename: 'Bar' }], __typename: 'Foo' }} | ${{ bar: [{}] }}
- ${[]} | ${[]}
- ${[{ __typename: 'Foo' }]} | ${[{}]}
- ${[{ bar: [{ a: 1, __typename: 'Bar' }] }]} | ${[{ bar: [{ a: 1 }] }]}
- `('given $input returns $expected, with all __typename keys removed', ({ input, expected }) => {
- const actual = stripTypenames(input);
- expect(actual).toEqual(expected);
- expect(input).not.toBe(actual);
- });
-
- it('given null returns null', () => {
- expect(stripTypenames(null)).toEqual(null);
- });
-});
diff --git a/spec/frontend/__helpers__/graphql_transformer.js b/spec/frontend/__helpers__/graphql_transformer.js
index e776e2ea6ac..f26b63dadfd 100644
--- a/spec/frontend/__helpers__/graphql_transformer.js
+++ b/spec/frontend/__helpers__/graphql_transformer.js
@@ -3,6 +3,8 @@ const loader = require('graphql-tag/loader');
module.exports = {
process(src) {
- return loader.call({ cacheable() {} }, src);
+ return {
+ code: loader.call({ cacheable() {} }, src),
+ };
},
};
diff --git a/spec/frontend/__helpers__/jest_helpers.js b/spec/frontend/__helpers__/jest_helpers.js
deleted file mode 100644
index 273d2c91966..00000000000
--- a/spec/frontend/__helpers__/jest_helpers.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
-@module
-
-This method provides convenience functions to help migrating from Karma/Jasmine to Jest.
-
-Try not to use these in new tests - this module is provided primarily for convenience of migrating tests.
- */
-
-/**
- * Creates a plain JS object pre-populated with Jest spy functions. Useful for making simple mocks classes.
- *
- * @see https://jasmine.github.io/2.0/introduction.html#section-Spies:_%3Ccode%3EcreateSpyObj%3C/code%3E
- * @param {string} baseName Human-readable name of the object. This is used for reporting purposes.
- * @param methods {string[]} List of method names that will be added to the spy object.
- */
-export function createSpyObj(baseName, methods) {
- const obj = {};
- methods.forEach((method) => {
- obj[method] = jest.fn().mockName(`${baseName}#${method}`);
- });
- return obj;
-}
diff --git a/spec/frontend/__helpers__/mock_window_location_helper.js b/spec/frontend/__helpers__/mock_window_location_helper.js
index 14082857053..de1e8c99b54 100644
--- a/spec/frontend/__helpers__/mock_window_location_helper.js
+++ b/spec/frontend/__helpers__/mock_window_location_helper.js
@@ -21,18 +21,31 @@ const useMockLocation = (fn) => {
afterEach(() => {
currentWindowLocation = origWindowLocation;
});
+
+ return () => {
+ beforeEach(() => {
+ currentWindowLocation = origWindowLocation;
+ });
+ };
};
/**
* Create an object with the location interface but `jest.fn()` implementations.
*/
export const createWindowLocationSpy = () => {
- return {
+ const { origin, href } = window.location;
+
+ const mockLocation = {
assign: jest.fn(),
reload: jest.fn(),
replace: jest.fn(),
toString: jest.fn(),
+ origin,
+ // TODO: Do we need to update `origin` if `href` is changed?
+ href,
};
+
+ return mockLocation;
};
/**
diff --git a/spec/frontend/__helpers__/raw_transformer.js b/spec/frontend/__helpers__/raw_transformer.js
index 09101b7a64f..3b0bed14e8d 100644
--- a/spec/frontend/__helpers__/raw_transformer.js
+++ b/spec/frontend/__helpers__/raw_transformer.js
@@ -1,6 +1,6 @@
/* eslint-disable import/no-commonjs */
module.exports = {
process: (content) => {
- return `module.exports = ${JSON.stringify(content)}`;
+ return { code: `module.exports = ${JSON.stringify(content)}` };
},
};
diff --git a/spec/frontend/__helpers__/set_timeout_promise_helper.js b/spec/frontend/__helpers__/set_timeout_promise_helper.js
deleted file mode 100644
index afd18d92d15..00000000000
--- a/spec/frontend/__helpers__/set_timeout_promise_helper.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default (time = 0) =>
- new Promise((resolve) => {
- setTimeout(resolve, time);
- });
diff --git a/spec/frontend/__helpers__/web_worker_transformer.js b/spec/frontend/__helpers__/web_worker_transformer.js
index 767ab3f5675..86be856f7b7 100644
--- a/spec/frontend/__helpers__/web_worker_transformer.js
+++ b/spec/frontend/__helpers__/web_worker_transformer.js
@@ -1,18 +1,22 @@
/* eslint-disable import/no-commonjs */
-const babelJestTransformer = require('babel-jest');
+const { createTransformer } = require('babel-jest');
// This Jest will transform the code of a WebWorker module into a FakeWebWorker subclass.
// This is meant to mirror Webpack's [`worker-loader`][1].
// [1]: https://webpack.js.org/loaders/worker-loader/
module.exports = {
process: (contentArg, filename, ...args) => {
- const { code: content } = babelJestTransformer.default.process(contentArg, filename, ...args);
+ const { code: content } = createTransformer().process(contentArg, filename, ...args);
- return `const { FakeWebWorker } = require("helpers/web_worker_fake");
+ const jestTransformedWorkerCode = `const { FakeWebWorker } = require("helpers/web_worker_fake");
module.exports = class JestTransformedWorker extends FakeWebWorker {
constructor() {
super(${JSON.stringify(filename)}, ${JSON.stringify(content)});
}
};`;
+
+ return {
+ code: jestTransformedWorkerCode,
+ };
},
};
diff --git a/spec/frontend/__helpers__/yaml_transformer.js b/spec/frontend/__helpers__/yaml_transformer.js
index a23f9b1f715..e0b4d01f542 100644
--- a/spec/frontend/__helpers__/yaml_transformer.js
+++ b/spec/frontend/__helpers__/yaml_transformer.js
@@ -6,6 +6,6 @@ module.exports = {
process: (sourceContent) => {
const jsonContent = JsYaml.load(sourceContent);
const json = JSON.stringify(jsonContent);
- return `module.exports = ${json}`;
+ return { code: `module.exports = ${json}` };
},
};
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 6f2888e5c42..4d893bcd0bd 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -49,6 +49,8 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
'boundary',
'container',
'showCloseButton',
+ 'show',
+ 'boundaryPadding',
].map((prop) => [prop, {}]),
),
},
diff --git a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
index 3778943872e..212f4c0842c 100644
--- a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
+++ b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
@@ -1,4 +1,4 @@
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BackgroundMigrationsDatabaseListbox from '~/admin/background_migrations/components/database_listbox.vue';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
@@ -30,15 +30,15 @@ describe('BackgroundMigrationsDatabaseListbox', () => {
wrapper.destroy();
});
- const findGlListbox = () => wrapper.findComponent(GlListbox);
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
describe('template always', () => {
beforeEach(() => {
createComponent();
});
- it('renders GlListbox', () => {
- expect(findGlListbox().exists()).toBe(true);
+ it('renders GlCollapsibleListbox', () => {
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
});
});
@@ -48,7 +48,7 @@ describe('BackgroundMigrationsDatabaseListbox', () => {
});
it('selecting a listbox item fires visitUrl with the database param', () => {
- findGlListbox().vm.$emit('select', MOCK_DATABASES[1].value);
+ findGlCollapsibleListbox().vm.$emit('select', MOCK_DATABASES[1].value);
expect(setUrlParams).toHaveBeenCalledWith({ database: MOCK_DATABASES[1].value });
expect(visitUrl).toHaveBeenCalled();
diff --git a/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js b/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js
new file mode 100644
index 00000000000..291c3aed1cf
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js
@@ -0,0 +1,46 @@
+import { mount } from '@vue/test-utils';
+import { GlDatepicker } from '@gitlab/ui';
+import DatetimePicker from '~/admin/broadcast_messages/components/datetime_picker.vue';
+
+describe('DatetimePicker', () => {
+ let wrapper;
+
+ const toDate = (day, time) => new Date(`${day}T${time}:00.000Z`);
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
+ const findTimepicker = () => wrapper.findComponent('[data-testid="time-picker"]');
+
+ const testDay = '2022-03-22';
+ const testTime = '01:23';
+
+ function createComponent() {
+ wrapper = mount(DatetimePicker, {
+ propsData: {
+ value: toDate(testDay, testTime),
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the Date in the datepicker and timepicker inputs', () => {
+ expect(findDatepicker().props().value).toEqual(toDate(testDay, testTime));
+ expect(findTimepicker().element.value).toEqual(testTime);
+ });
+
+ it('emits Date with the new day/old time when the date picker changes', () => {
+ const newDay = '1992-06-30';
+ const newTime = '08:00';
+
+ findDatepicker().vm.$emit('input', toDate(newDay, newTime));
+ expect(wrapper.emitted().input).toEqual([[toDate(newDay, testTime)]]);
+ });
+
+ it('emits Date with the old day/new time when the time picker changes', () => {
+ const newTime = '08:00';
+
+ findTimepicker().vm.$emit('input', newTime);
+ expect(wrapper.emitted().input).toEqual([[toDate(testDay, newTime)]]);
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
new file mode 100644
index 00000000000..88ea79f38b3
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -0,0 +1,201 @@
+import { mount } from '@vue/test-utils';
+import { GlBroadcastMessage, GlForm } from '@gitlab/ui';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { createAlert } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import MessageForm from '~/admin/broadcast_messages/components/message_form.vue';
+import {
+ BROADCAST_MESSAGES_PATH,
+ TYPE_BANNER,
+ TYPE_NOTIFICATION,
+ THEMES,
+} from '~/admin/broadcast_messages/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('MessageForm', () => {
+ let wrapper;
+ let axiosMock;
+
+ const defaultProps = {
+ message: 'zzzzzzz',
+ broadcastType: TYPE_BANNER,
+ theme: THEMES[0].value,
+ dismissable: false,
+ targetPath: '',
+ targetAccessLevels: [],
+ startsAt: new Date(),
+ endsAt: new Date(),
+ };
+
+ const findPreview = () => extendedWrapper(wrapper.findComponent(GlBroadcastMessage));
+ const findThemeSelect = () => wrapper.findComponent('[data-testid=theme-select]');
+ const findDismissable = () => wrapper.findComponent('[data-testid=dismissable-checkbox]');
+ const findTargetRoles = () => wrapper.findComponent('[data-testid=target-roles-checkboxes]');
+ const findSubmitButton = () => wrapper.findComponent('[data-testid=submit-button]');
+ const findForm = () => wrapper.findComponent(GlForm);
+
+ function createComponent({ broadcastMessage = {}, glFeatures = {} }) {
+ wrapper = mount(MessageForm, {
+ provide: {
+ glFeatures,
+ targetAccessLevelOptions: MOCK_TARGET_ACCESS_LEVELS,
+ },
+ propsData: {
+ broadcastMessage: {
+ ...defaultProps,
+ ...broadcastMessage,
+ },
+ },
+ });
+ }
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ createAlert.mockClear();
+ });
+
+ describe('the message preview', () => {
+ it('renders the preview with the user selected theme', () => {
+ const theme = 'blue';
+ createComponent({ broadcastMessage: { theme } });
+ expect(findPreview().props().theme).toEqual(theme);
+ });
+
+ it('renders the placeholder text when the user message is blank', () => {
+ createComponent({ broadcastMessage: { message: ' ' } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.messagePlaceholder);
+ });
+ });
+
+ describe('theme select dropdown', () => {
+ it('renders for Banners', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_BANNER } });
+ expect(findThemeSelect().exists()).toBe(true);
+ });
+
+ it('does not render for Notifications', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_NOTIFICATION } });
+ expect(findThemeSelect().exists()).toBe(false);
+ });
+ });
+
+ describe('dismissable checkbox', () => {
+ it('renders for Banners', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_BANNER } });
+ expect(findDismissable().exists()).toBe(true);
+ });
+
+ it('does not render for Notifications', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_NOTIFICATION } });
+ expect(findDismissable().exists()).toBe(false);
+ });
+ });
+
+ describe('target roles checkboxes', () => {
+ it('renders when roleTargetedBroadcastMessages feature is enabled', () => {
+ createComponent({ glFeatures: { roleTargetedBroadcastMessages: true } });
+ expect(findTargetRoles().exists()).toBe(true);
+ });
+
+ it('does not render when roleTargetedBroadcastMessages feature is disabled', () => {
+ createComponent({ glFeatures: { roleTargetedBroadcastMessages: false } });
+ expect(findTargetRoles().exists()).toBe(false);
+ });
+ });
+
+ describe('form submit button', () => {
+ it('renders the "add" text when the message is not persisted', () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.add);
+ });
+
+ it('renders the "update" text when the message is persisted', () => {
+ createComponent({ broadcastMessage: { id: 100 } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.update);
+ });
+
+ it('is disabled when the user message is blank', () => {
+ createComponent({ broadcastMessage: { message: ' ' } });
+ expect(findSubmitButton().props().disabled).toBe(true);
+ });
+
+ it('is not disabled when the user message is present', () => {
+ createComponent({ broadcastMessage: { message: 'alsdjfkldsj' } });
+ expect(findSubmitButton().props().disabled).toBe(false);
+ });
+ });
+
+ describe('form submission', () => {
+ const defaultPayload = {
+ message: defaultProps.message,
+ broadcast_type: defaultProps.broadcastType,
+ theme: defaultProps.theme,
+ dismissable: defaultProps.dismissable,
+ target_path: defaultProps.targetPath,
+ target_access_levels: defaultProps.targetAccessLevels,
+ starts_at: defaultProps.startsAt,
+ ends_at: defaultProps.endsAt,
+ };
+
+ it('sends a create request for a new message form', async () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(axiosMock.history.post).toHaveLength(1);
+ expect(axiosMock.history.post[0]).toMatchObject({
+ url: BROADCAST_MESSAGES_PATH,
+ data: JSON.stringify(defaultPayload),
+ });
+ });
+
+ it('shows an error alert if the create request fails', async () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ axiosMock.onPost(BROADCAST_MESSAGES_PATH).replyOnce(httpStatus.BAD_REQUEST);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: wrapper.vm.$options.i18n.addError,
+ }),
+ );
+ });
+
+ it('sends an update request for a persisted message form', async () => {
+ const id = 1337;
+ createComponent({ broadcastMessage: { id } });
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(axiosMock.history.patch).toHaveLength(1);
+ expect(axiosMock.history.patch[0]).toMatchObject({
+ url: `${BROADCAST_MESSAGES_PATH}/${id}`,
+ data: JSON.stringify(defaultPayload),
+ });
+ });
+
+ it('shows an error alert if the update request fails', async () => {
+ const id = 1337;
+ createComponent({ broadcastMessage: { id } });
+ axiosMock.onPost(`${BROADCAST_MESSAGES_PATH}/${id}`).replyOnce(httpStatus.BAD_REQUEST);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: wrapper.vm.$options.i18n.updateError,
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/mock_data.js b/spec/frontend/admin/broadcast_messages/mock_data.js
index 8dd98c2319d..2e20b5cf638 100644
--- a/spec/frontend/admin/broadcast_messages/mock_data.js
+++ b/spec/frontend/admin/broadcast_messages/mock_data.js
@@ -15,3 +15,11 @@ export const generateMockMessages = (n) =>
[...Array(n).keys()].map((id) => generateMockMessage(id + 1));
export const MOCK_MESSAGES = generateMockMessages(5).map((id) => generateMockMessage(id));
+
+export const MOCK_TARGET_ACCESS_LEVELS = [
+ ['Guest', 10],
+ ['Reporter', 20],
+ ['Developer', 30],
+ ['Maintainer', 40],
+ ['Owner', 50],
+];
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 e6718f62b91..f2a951bcc76 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -54,7 +54,6 @@ describe('Signup Form', () => {
prop | propValue | elementSelector | formElementPassedDataType | formElementKey | expected
${'signupEnabled'} | ${mockData.signupEnabled} | ${'[name="application_setting[signup_enabled]"]'} | ${'prop'} | ${'value'} | ${mockData.signupEnabled}
${'requireAdminApprovalAfterUserSignup'} | ${mockData.requireAdminApprovalAfterUserSignup} | ${'[name="application_setting[require_admin_approval_after_user_signup]"]'} | ${'prop'} | ${'value'} | ${mockData.requireAdminApprovalAfterUserSignup}
- ${'sendUserConfirmationEmail'} | ${mockData.sendUserConfirmationEmail} | ${'[name="application_setting[send_user_confirmation_email]"]'} | ${'prop'} | ${'value'} | ${mockData.sendUserConfirmationEmail}
${'newUserSignupsCap'} | ${mockData.newUserSignupsCap} | ${'[name="application_setting[new_user_signups_cap]"]'} | ${'attribute'} | ${'value'} | ${mockData.newUserSignupsCap}
${'minimumPasswordLength'} | ${mockData.minimumPasswordLength} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'value'} | ${mockData.minimumPasswordLength}
${'minimumPasswordLengthMin'} | ${mockData.minimumPasswordLengthMin} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'min'} | ${mockData.minimumPasswordLengthMin}
diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js
index dd1ed317497..ce5ec2248fe 100644
--- a/spec/frontend/admin/signup_restrictions/mock_data.js
+++ b/spec/frontend/admin/signup_restrictions/mock_data.js
@@ -3,7 +3,6 @@ export const rawMockData = {
settingsPath: 'path/to/settings',
signupEnabled: 'true',
requireAdminApprovalAfterUserSignup: 'true',
- sendUserConfirmationEmail: 'true',
emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
@@ -32,7 +31,6 @@ export const mockData = {
settingsPath: 'path/to/settings',
signupEnabled: true,
requireAdminApprovalAfterUserSignup: true,
- sendUserConfirmationEmail: true,
emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
diff --git a/spec/frontend/admin/signup_restrictions/utils_spec.js b/spec/frontend/admin/signup_restrictions/utils_spec.js
index f07e14430f9..e393b07baa9 100644
--- a/spec/frontend/admin/signup_restrictions/utils_spec.js
+++ b/spec/frontend/admin/signup_restrictions/utils_spec.js
@@ -10,7 +10,6 @@ describe('utils', () => {
booleanAttributes: [
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
- 'sendUserConfirmationEmail',
'domainDenylistEnabled',
'denylistTypeRawSelected',
'emailRestrictionsEnabled',
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 4693d5a47e4..bff4905a12c 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
@@ -16,7 +16,7 @@ exports[`Alert integration settings form default state should match the default
>
<gl-form-checkbox-stub
checked="true"
- data-qa-selector="create_issue_checkbox"
+ data-qa-selector="create_incident_checkbox"
id="2"
>
<span>
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index fcefcb7cf66..62a3e07186a 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -32,7 +32,7 @@ import {
} from '~/alerts_settings/utils/error_messages';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
createHttpVariables,
updateHttpVariables,
@@ -358,7 +358,7 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows an error alert when integration test payload is invalid', async () => {
- mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY);
+ mock.onPost(/(.*)/).replyOnce(HTTP_STATUS_UNPROCESSABLE_ENTITY);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createAlert).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
expect(createAlert).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap b/spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap
index 92927ef16ec..92927ef16ec 100644
--- a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap
+++ b/spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/analytics/cycle_analytics/base_spec.js
index 013bea671a8..58588ff49ce 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/base_spec.js
@@ -4,12 +4,12 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
-import BaseComponent from '~/cycle_analytics/components/base.vue';
-import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
-import StageTable from '~/cycle_analytics/components/stage_table.vue';
-import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
-import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
-import initState from '~/cycle_analytics/store/state';
+import BaseComponent from '~/analytics/cycle_analytics/components/base.vue';
+import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
+import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
+import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
+import { NOT_ENOUGH_DATA_ERROR } from '~/analytics/cycle_analytics/constants';
+import initState from '~/analytics/cycle_analytics/store/state';
import {
transformedProjectStagePathData,
selectedStage,
diff --git a/spec/frontend/cycle_analytics/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
index 36933790cf7..2b26b202882 100644
--- a/spec/frontend/cycle_analytics/filter_bar_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
@@ -7,10 +7,16 @@ import {
filterMilestones,
filterLabels,
} from 'jest/vue_shared/components/filtered_search_bar/store/modules/filters/mock_data';
-import FilterBar from '~/cycle_analytics/components/filter_bar.vue';
-import storeConfig from '~/cycle_analytics/store';
+import FilterBar from '~/analytics/cycle_analytics/components/filter_bar.vue';
+import storeConfig from '~/analytics/cycle_analytics/store';
import * as commonUtils from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
+import {
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import * as utils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import initialFiltersState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
@@ -18,10 +24,10 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
Vue.use(Vuex);
-const milestoneTokenType = 'milestone';
-const labelsTokenType = 'labels';
-const authorTokenType = 'author';
-const assigneesTokenType = 'assignees';
+const milestoneTokenType = TOKEN_TYPE_MILESTONE;
+const labelsTokenType = TOKEN_TYPE_LABEL;
+const authorTokenType = TOKEN_TYPE_AUTHOR;
+const assigneesTokenType = TOKEN_TYPE_ASSIGNEE;
const initialFilterBarState = {
selectedMilestone: null,
@@ -162,8 +168,8 @@ describe('Filter bar', () => {
it('clicks on the search button, setFilters is dispatched', () => {
const filters = [
- { type: 'milestone', value: { data: selectedMilestone[0].title, operator: '=' } },
- { type: 'labels', value: { data: selectedLabelList[0].title, operator: '=' } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: selectedMilestone[0].title, operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: selectedLabelList[0].title, operator: '=' } },
];
findFilteredSearch().vm.$emit('onFilter', filters);
diff --git a/spec/frontend/cycle_analytics/formatted_stage_count_spec.js b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
index 1228b8511ea..9be92bb92bc 100644
--- a/spec/frontend/cycle_analytics/formatted_stage_count_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Component from '~/cycle_analytics/components/formatted_stage_count.vue';
+import Component from '~/analytics/cycle_analytics/components/formatted_stage_count.vue';
describe('Formatted Stage Count', () => {
let wrapper = null;
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index 02666260cdb..f820f755400 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -12,7 +12,7 @@ import {
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
-} from '~/cycle_analytics/constants';
+} from '~/analytics/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
diff --git a/spec/frontend/cycle_analytics/path_navigation_spec.js b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
index fec1526359c..107e62035c3 100644
--- a/spec/frontend/cycle_analytics/path_navigation_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
@@ -2,7 +2,7 @@ import { GlPath, GlSkeletonLoader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Component from '~/cycle_analytics/components/path_navigation.vue';
+import Component from '~/analytics/cycle_analytics/components/path_navigation.vue';
import { transformedProjectStagePathData, selectedStage } from './mock_data';
describe('Project PathNavigation', () => {
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
index 473e1d5b664..cfccce7eae9 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
@@ -3,8 +3,8 @@ import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import StageTable from '~/cycle_analytics/components/stage_table.vue';
-import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants';
+import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
+import { PAGINATION_SORT_FIELD_DURATION } from '~/analytics/cycle_analytics/constants';
import { issueEvents, issueStage, reviewStage, reviewEvents } from './mock_data';
let wrapper = null;
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
index 94b6de85a5c..f87807804c9 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
@@ -1,8 +1,8 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import * as actions from '~/cycle_analytics/store/actions';
-import * as getters from '~/cycle_analytics/store/getters';
+import * as actions from '~/analytics/cycle_analytics/store/actions';
+import * as getters from '~/analytics/cycle_analytics/store/getters';
import httpStatusCodes from '~/lib/utils/http_status';
import {
allowedStages,
diff --git a/spec/frontend/cycle_analytics/store/getters_spec.js b/spec/frontend/analytics/cycle_analytics/store/getters_spec.js
index c9208045a68..8ad1e1b27de 100644
--- a/spec/frontend/cycle_analytics/store/getters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/getters_spec.js
@@ -1,4 +1,4 @@
-import * as getters from '~/cycle_analytics/store/getters';
+import * as getters from '~/analytics/cycle_analytics/store/getters';
import {
allowedStages,
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
index 2e9e5d91471..567fac81e1f 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
@@ -1,10 +1,10 @@
import { useFakeDate } from 'helpers/fake_date';
-import * as types from '~/cycle_analytics/store/mutation_types';
-import mutations from '~/cycle_analytics/store/mutations';
+import * as types from '~/analytics/cycle_analytics/store/mutation_types';
+import mutations from '~/analytics/cycle_analytics/store/mutations';
import {
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_DIRECTION_DESC,
-} from '~/cycle_analytics/constants';
+} from '~/analytics/cycle_analytics/constants';
import {
selectedStage,
rawIssueEvents,
diff --git a/spec/frontend/cycle_analytics/total_time_spec.js b/spec/frontend/analytics/cycle_analytics/total_time_spec.js
index 8cf9feab6e9..47ee7aad8c4 100644
--- a/spec/frontend/cycle_analytics/total_time_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/total_time_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import TotalTime from '~/cycle_analytics/components/total_time.vue';
+import TotalTime from '~/analytics/cycle_analytics/components/total_time.vue';
describe('TotalTime', () => {
let wrapper = null;
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/analytics/cycle_analytics/utils_spec.js
index 51405a1ba4d..fe412bf7498 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/utils_spec.js
@@ -4,7 +4,7 @@ import {
formatMedianValues,
filterStagesByHiddenStatus,
buildCycleAnalyticsInitialData,
-} from '~/cycle_analytics/utils';
+} from '~/analytics/cycle_analytics/utils';
import {
selectedStage,
allowedStages,
diff --git a/spec/frontend/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
index 6e96a6d756a..4f333e95d89 100644
--- a/spec/frontend/cycle_analytics/value_stream_filters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import Daterange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
-import FilterBar from '~/cycle_analytics/components/filter_bar.vue';
-import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
+import FilterBar from '~/analytics/cycle_analytics/components/filter_bar.vue';
+import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
import {
createdAfter as startDate,
createdBefore as endDate,
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
index 948dc5c9be2..948dc5c9be2 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 1f92010b771..5209d9c2d2c 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1,7 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, {
+ HTTP_STATUS_ACCEPTED,
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_NO_CONTENT,
+} from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -1069,7 +1073,7 @@ describe('Api', () => {
describe('when the release is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, release).replyOnce(httpStatus.CREATED);
+ mock.onPost(expectedUrl, release).replyOnce(HTTP_STATUS_CREATED);
return Api.createRelease(dummyProjectPath, release).then(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1125,7 +1129,7 @@ describe('Api', () => {
describe('when the Release is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, expectedLink).replyOnce(httpStatus.CREATED);
+ mock.onPost(expectedUrl, expectedLink).replyOnce(HTTP_STATUS_CREATED);
return Api.createReleaseLink(dummyProjectPath, dummyTagName, expectedLink).then(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1224,7 +1228,7 @@ describe('Api', () => {
describe('when the merge request is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED);
+ mock.onPost(expectedUrl, options).replyOnce(HTTP_STATUS_CREATED);
return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1332,7 +1336,7 @@ describe('Api', () => {
describe('when the freeze period is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED, expectedResult);
+ mock.onPost(expectedUrl, options).replyOnce(HTTP_STATUS_CREATED, expectedResult);
return Api.createFreezePeriod(projectId, options).then(({ data }) => {
expect(data).toStrictEqual(expectedResult);
@@ -1598,7 +1602,7 @@ describe('Api', () => {
const secureFileId = 2;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/secure_files/${secureFileId}`;
- mock.onDelete(expectedUrl).reply(httpStatus.NO_CONTENT, '');
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_NO_CONTENT, '');
const { data } = await Api.deleteProjectSecureFile(projectId, secureFileId);
expect(data).toEqual('');
});
@@ -1609,10 +1613,10 @@ describe('Api', () => {
const groupId = 1;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/dependency_proxy/cache`;
- mock.onDelete(expectedUrl).reply(httpStatus.ACCEPTED);
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED);
const { status } = await Api.deleteDependencyProxyCacheList(groupId, {});
- expect(status).toBe(httpStatus.ACCEPTED);
+ expect(status).toBe(HTTP_STATUS_ACCEPTED);
});
});
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 03ecbc01a56..2dfcdd551a1 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,19 +1,20 @@
import { nextTick } from 'vue';
import { GlButton, GlBadge } from '@gitlab/ui';
-import { getByRole } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
-import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const NoteableNoteStub = stubComponent(NoteableNote, {
template: `
<div>
<slot name="note-header-info">Test</slot>
+ <slot name="after-note-body">Test</slot>
</div>
`,
});
@@ -29,7 +30,6 @@ describe('Batch comments draft note component', () => {
},
};
- const getList = () => getByRole(wrapper.element, 'list');
const findSubmitReviewButton = () => wrapper.findComponent(PublishButton);
const findAddCommentButton = () => wrapper.findComponent(GlButton);
@@ -189,7 +189,7 @@ describe('Batch comments draft note component', () => {
});
it(`calls store ${expectedCalls.length} times on ${event}`, () => {
- getList().dispatchEvent(new MouseEvent(event, { bubbles: true }));
+ wrapper.element.dispatchEvent(new MouseEvent(event, { bubbles: true }));
expect(store.dispatch.mock.calls).toEqual(expectedCalls);
});
});
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 6a104f0c787..6a99294f855 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -3,9 +3,10 @@ import PreviewItem from '~/batch_comments/components/preview_item.vue';
import { createStore } from '~/batch_comments/stores';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
-import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('Batch comments draft preview item component', () => {
let wrapper;
let draft;
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index d1b7160d231..e89934c0192 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -4,9 +4,10 @@ import Vue from 'vue';
import Vuex from 'vuex';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import { createStore } from '~/mr_notes/stores';
-import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
Vue.use(Vuex);
describe('Batch comments publish dropdown component', () => {
diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js
new file mode 100644
index 00000000000..c87d11742dc
--- /dev/null
+++ b/spec/frontend/behaviors/markdown/render_observability_spec.js
@@ -0,0 +1,38 @@
+import renderObservability from '~/behaviors/markdown/render_observability';
+import * as ColorUtils from '~/lib/utils/color_utils';
+
+describe('Observability iframe renderer', () => {
+ const findObservabilityIframes = (theme = 'light') =>
+ document.querySelectorAll(`iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk"]`);
+
+ const renderEmbeddedObservability = () => {
+ renderObservability([...document.querySelectorAll('.js-render-observability')]);
+ jest.runAllTimers();
+ };
+
+ beforeEach(() => {
+ document.body.dataset.page = '';
+ document.body.innerHTML = '';
+ });
+
+ it('renders an observability iframe', () => {
+ document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
+
+ expect(findObservabilityIframes()).toHaveLength(0);
+
+ renderEmbeddedObservability();
+
+ expect(findObservabilityIframes()).toHaveLength(1);
+ });
+
+ it('renders iframe with dark param when GL has dark theme', () => {
+ document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
+ jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
+
+ expect(findObservabilityIframes('dark')).toHaveLength(0);
+
+ renderEmbeddedObservability();
+
+ expect(findObservabilityIframes('dark')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/blob/openapi/index_spec.js b/spec/frontend/blob/openapi/index_spec.js
index 17e718df495..d9d65258516 100644
--- a/spec/frontend/blob/openapi/index_spec.js
+++ b/spec/frontend/blob/openapi/index_spec.js
@@ -21,7 +21,7 @@ describe('OpenAPI blob viewer', () => {
it('initializes SwaggerUI with the correct configuration', () => {
expect(document.body.innerHTML).toContain(
- '<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups" frameborder="0" width="100%" height="1000"></iframe>',
+ '<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>',
);
});
});
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index 644539308c2..ed42322b0e6 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -5,8 +5,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
import SourceEditor from '~/blob_edit/edit_blob';
+import { createAlert } from '~/flash';
jest.mock('~/blob_edit/edit_blob');
+jest.mock('~/flash');
describe('BlobBundle', () => {
it('does not load SourceEditor by default', () => {
@@ -93,4 +95,26 @@ describe('BlobBundle', () => {
});
});
});
+
+ describe('Error handling', () => {
+ let message;
+ beforeEach(() => {
+ setHTMLFixture(`<div class="js-edit-blob-form" data-blob-filename="blah"></div>`);
+ message = 'Foo';
+ SourceEditor.mockImplementation(() => {
+ throw new Error(message);
+ });
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ SourceEditor.mockClear();
+ });
+
+ it('correctly outputs error message when it occurs', async () => {
+ blobBundle();
+ await waitForPromises();
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
});
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 3a2beb714e9..34c0504143c 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -198,6 +198,13 @@ describe('Board list component', () => {
expect(findDraggable().exists()).toBe(true);
});
+ it('sets delay and delayOnTouchOnly attributes on board list', () => {
+ const listEl = wrapper.findComponent({ ref: 'list' });
+
+ expect(listEl.attributes('delay')).toBe('100');
+ expect(listEl.attributes('delayontouchonly')).toBe('true');
+ });
+
describe('handleDragOnStart', () => {
it('adds a class `is-dragging` to document body', () => {
expect(document.body.classList.contains('is-dragging')).toBe(false);
@@ -269,6 +276,10 @@ describe('Board list component', () => {
it('Draggable is not used', () => {
expect(findDraggable().exists()).toBe(false);
});
+
+ it('Board card move to position is not visible', () => {
+ expect(findMoveToPositionComponent().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 7e35c39cd48..0d5b1d16e30 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -12,7 +12,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
-import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
Vue.use(Vuex);
@@ -146,6 +146,20 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(false);
});
+ it('does not render SidebarHealthStatusWidget', async () => {
+ const SidebarHealthStatusWidget = (
+ await import('ee_component/sidebar/components/health_status/sidebar_health_status_widget.vue')
+ ).default;
+ expect(wrapper.findComponent(SidebarHealthStatusWidget).exists()).toBe(false);
+ });
+
+ it('does not render SidebarWeightWidget', async () => {
+ const SidebarWeightWidget = (
+ await import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue')
+ ).default;
+ expect(wrapper.findComponent(SidebarWeightWidget).exists()).toBe(false);
+ });
+
describe('when we emit close', () => {
let toggleBoardItem;
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index b2138700602..82e7ab48e7d 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -123,15 +123,39 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
- it('resizes the list on resize', async () => {
+ it('on small screens, sets board container height to full height', async () => {
window.innerHeight = 1000;
+ window.innerWidth = 767;
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
wrapper.vm.resizeObserver.trigger();
await nextTick();
- expect(wrapper.findComponent({ ref: 'list' }).attributes('style')).toBe('height: 900px;');
+ const style = wrapper.findComponent({ ref: 'list' }).attributes('style');
+
+ expect(style).toBe('height: 1000px;');
+ });
+
+ it('on large screens, sets board container height fill area below filters', async () => {
+ window.innerHeight = 1000;
+ window.innerWidth = 768;
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
+
+ wrapper.vm.resizeObserver.trigger();
+
+ await nextTick();
+
+ const style = wrapper.findComponent({ ref: 'list' }).attributes('style');
+
+ expect(style).toBe('height: 900px;');
+ });
+
+ it('sets delay and delayOnTouchOnly attributes on board list', () => {
+ const listEl = wrapper.findComponent({ ref: 'list' });
+
+ expect(listEl.attributes('delay')).toBe('100');
+ expect(listEl.attributes('delayontouchonly')).toBe('true');
});
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 6f17e4193a3..e80c66f7fb8 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -17,7 +17,7 @@ import {
TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { createStore } from '~/boards/stores';
@@ -30,7 +30,7 @@ describe('BoardFilteredSearch', () => {
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
- type: 'label',
+ type: TOKEN_TYPE_LABEL,
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
@@ -43,15 +43,15 @@ describe('BoardFilteredSearch', () => {
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
- type: 'author',
+ type: TOKEN_TYPE_AUTHOR,
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
symbol: '@',
- token: AuthorToken,
+ token: UserToken,
unique: true,
- fetchAuthors: () => new Promise(() => {}),
+ fetchUsers: () => new Promise(() => {}),
},
];
@@ -109,7 +109,7 @@ describe('BoardFilteredSearch', () => {
createComponent({ props: { eeFilters: { labelName: ['label'] } } });
expect(findFilteredSearch().props('initialFilterValue')).toEqual([
- { type: 'label', value: { data: 'label', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } },
]);
});
});
@@ -158,7 +158,9 @@ describe('BoardFilteredSearch', () => {
['None', url('None')],
['Any', url('Any')],
])('sets the url param %s', (assigneeParam, expected) => {
- const mockFilters = [{ type: 'assignee', value: { data: assigneeParam, operator: '=' } }];
+ const mockFilters = [
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: assigneeParam, operator: '=' } },
+ ];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index e4a6a2b8b76..513561307cd 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -23,14 +23,14 @@ describe('IssueBoardFilter', () => {
});
};
- let fetchAuthorsSpy;
+ let fetchUsersSpy;
let fetchLabelsSpy;
beforeEach(() => {
- fetchAuthorsSpy = jest.fn();
+ fetchUsersSpy = jest.fn();
fetchLabelsSpy = jest.fn();
issueBoardFilters.mockReturnValue({
- fetchAuthors: fetchAuthorsSpy,
+ fetchUsers: fetchUsersSpy,
fetchLabels: fetchLabelsSpy,
});
});
@@ -59,7 +59,7 @@ describe('IssueBoardFilter', () => {
const tokens = mockTokens(
fetchLabelsSpy,
- fetchAuthorsSpy,
+ fetchUsersSpy,
wrapper.vm.fetchMilestones,
isSignedIn,
);
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
index 5c435643425..e2e4baefad0 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
@@ -42,13 +42,20 @@ describe('BoardSidebarTimeTracker', () => {
wrapper = null;
});
- it.each([[true], [false]])(
- 'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=%s)',
- (timeTrackingLimitToHours) => {
- createComponent({ provide: { timeTrackingLimitToHours } });
+ it.each`
+ timeTrackingLimitToHours | canUpdate
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${false}
+ ${false} | ${true}
+ `(
+ 'renders IssuableTimeTracker with correct spent and estimated time (timeTrackingLimitToHours=$timeTrackingLimitToHours, canUpdate=$canUpdate)',
+ ({ timeTrackingLimitToHours, canUpdate }) => {
+ createComponent({ provide: { timeTrackingLimitToHours, canUpdate } });
expect(wrapper.findComponent(IssuableTimeTracker).props()).toEqual({
limitToHours: timeTrackingLimitToHours,
+ canAddTimeEntries: canUpdate,
showCollapsed: false,
issuableId: '1',
issuableIid: '1',
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 3c26fa97338..df41eb05eae 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -2,22 +2,26 @@ import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
import {
- OPERATOR_IS_AND_IS_NOT,
- OPERATOR_IS_ONLY,
+ OPERATORS_IS,
+ OPERATORS_IS_NOT,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONFIDENTIAL,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -733,54 +737,54 @@ export const mockMoveData = {
};
export const mockEmojiToken = {
- type: 'my-reaction',
+ type: TOKEN_TYPE_MY_REACTION,
icon: 'thumb-up',
- title: 'My-Reaction',
+ title: TOKEN_TITLE_MY_REACTION,
unique: true,
token: EmojiToken,
fetchEmojis: expect.any(Function),
};
export const mockConfidentialToken = {
- type: 'confidential',
+ type: TOKEN_TYPE_CONFIDENTIAL,
icon: 'eye-slash',
- title: 'Confidential',
+ title: TOKEN_TITLE_CONFIDENTIAL,
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ icon: 'eye-slash', value: 'yes', title: 'Yes' },
{ icon: 'eye', value: 'no', title: 'No' },
],
};
-export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedIn) => [
+export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn) => [
{
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
type: TOKEN_TYPE_ASSIGNEE,
- operators: OPERATOR_IS_AND_IS_NOT,
- token: AuthorToken,
+ operators: OPERATORS_IS_NOT,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: [],
+ fetchUsers,
+ preloadedUsers: [],
},
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
type: TOKEN_TYPE_AUTHOR,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
symbol: '@',
- token: AuthorToken,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: [],
+ fetchUsers,
+ preloadedUsers: [],
},
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
type: TOKEN_TYPE_LABEL,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
token: LabelToken,
unique: false,
symbol: '~',
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index 7ff34ffdf9e..4324e7068e0 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -156,7 +156,7 @@ describe('ProjectSelect component', () => {
});
it('renders the name of the selected project', () => {
- expect(findGlDropdown().find('.gl-new-dropdown-button-text').text()).toBe(
+ expect(findGlDropdown().find('.gl-dropdown-button-text').text()).toBe(
mockProjectsList1[0].name,
);
});
diff --git a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
index 553ca52f9ce..b2a25bc93ea 100644
--- a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
+++ b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
@@ -4,7 +4,10 @@ import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_i
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, {
+ HTTP_STATUS_CONFLICT,
+ HTTP_STATUS_METHOD_NOT_ALLOWED,
+} from '~/lib/utils/http_status';
jest.mock('~/captcha/wait_for_captcha_to_be_solved');
@@ -33,7 +36,7 @@ describe('registerCaptchaModalInterceptor', () => {
mock.onAny('/endpoint-with-unrelated-error').reply(404, AXIOS_RESPONSE);
mock.onAny('/endpoint-with-captcha').reply((config) => {
if (!supportedMethods.includes(config.method)) {
- return [httpStatusCodes.METHOD_NOT_ALLOWED, { method: config.method }];
+ return [HTTP_STATUS_METHOD_NOT_ALLOWED, { method: config.method }];
}
const data = JSON.parse(config.data);
@@ -46,7 +49,7 @@ describe('registerCaptchaModalInterceptor', () => {
return [httpStatusCodes.OK, { ...data, method: config.method, CAPTCHA_SUCCESS }];
}
- return [httpStatusCodes.CONFLICT, NEEDS_CAPTCHA_RESPONSE];
+ return [HTTP_STATUS_CONFLICT, NEEDS_CAPTCHA_RESPONSE];
});
axios.interceptors.response.handlers = [];
@@ -123,7 +126,7 @@ describe('registerCaptchaModalInterceptor', () => {
await expect(() => axios[method]('/endpoint-with-captcha')).rejects.toThrow(
expect.objectContaining({
response: expect.objectContaining({
- status: httpStatusCodes.METHOD_NOT_ALLOWED,
+ status: HTTP_STATUS_METHOD_NOT_ALLOWED,
data: { method },
}),
}),
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
index ea69a80274e..d4f588a0e09 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
@@ -2,9 +2,9 @@ import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import CiLint from '~/ci_lint/components/ci_lint.vue';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
-import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import CiLint from '~/ci/ci_lint/components/ci_lint.vue';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
+import lintCIMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
import { mockLintDataValid } from '../mock_data';
diff --git a/spec/frontend/ci_lint/mock_data.js b/spec/frontend/ci/ci_lint/mock_data.js
index 660b2ad6e8b..05582470dfa 100644
--- a/spec/frontend/ci_lint/mock_data.js
+++ b/spec/frontend/ci/ci_lint/mock_data.js
@@ -1,4 +1,4 @@
-import { mockJobs } from 'jest/pipeline_editor/mock_data';
+import { mockJobs } from 'jest/ci/pipeline_editor/mock_data';
export const mockLintDataError = {
data: {
diff --git a/spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
index d03f12bc249..b00e1adab63 100644
--- a/spec/frontend/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
@@ -3,8 +3,8 @@ import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
-import { CODE_SNIPPET_SOURCE_API_FUZZING } from '~/pipeline_editor/components/code_snippet_alert/constants';
+import CodeSnippetAlert from '~/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
+import { CODE_SNIPPET_SOURCE_API_FUZZING } from '~/ci/pipeline_editor/components/code_snippet_alert/constants';
const apiFuzzingConfigurationPath = '/namespace/project/-/security/configuration/api_fuzzing';
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
index 0ee6da9d329..8e1d8081dd8 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
@@ -2,7 +2,7 @@ import { nextTick } from 'vue';
import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+import CommitForm from '~/ci/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
index 744b0378a75..f6e93c55bbb 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
@@ -4,18 +4,18 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
-import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
+import CommitForm from '~/ci/pipeline_editor/components/commit/commit_form.vue';
+import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
import {
COMMIT_ACTION_CREATE,
COMMIT_ACTION_UPDATE,
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
-} from '~/pipeline_editor/constants';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
-import commitCreate from '~/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import updatePipelineEtag from '~/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
+} from '~/ci/pipeline_editor/constants';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
+import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
import {
mockCiConfigPath,
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index 7e1e5004d91..137137ec657 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -1,8 +1,8 @@
import { getByRole } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
-import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
+import FirstPipelineCard from '~/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
+import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
describe('First pipeline card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
index c592e959068..cdce757ce7c 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
+import GettingStartedCard from '~/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue';
describe('Getting started card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
index 49177befe0e..6909916c3e6 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -1,8 +1,8 @@
import { getByRole } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
-import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
+import PipelineConfigReferenceCard from '~/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
+import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
describe('Pipeline config reference card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
index bebd2484c1d..0c6879020de 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
+import VisualizeAndLintCard from '~/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue';
describe('Visual and Lint card', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 33b53bf6a56..42e372cc1db 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlDrawer } from '@gitlab/ui';
-import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
+import PipelineEditorDrawer from '~/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
describe('Pipeline editor drawer', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
index edd2b45569a..f510c61ee74 100644
--- a/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue';
+import DemoJobPill from '~/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue';
describe('Demo job pill', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
index 7dd8a77d055..2a2bc2547cc 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
@@ -2,7 +2,7 @@ import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
-import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import CiConfigMergedPreview from '~/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import { mockLintResponse, mockCiConfigPath } from '../../mock_data';
describe('Text editor component', () => {
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index 930f08ef545..d7f0ce838d6 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
+import CiEditorHeader from '~/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import {
pipelineEditorTrackingOptions,
TEMPLATE_REPOSITORY_URL,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
describe('CI Editor Header', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
index 6cdf9a93d55..63e23c41263 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
-import { SOURCE_EDITOR_DEBOUNCE } from '~/pipeline_editor/constants';
-import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
+import { SOURCE_EDITOR_DEBOUNCE } from '~/ci/pipeline_editor/constants';
+import TextEditor from '~/ci/pipeline_editor/components/editor/text_editor.vue';
import {
mockCiConfigPath,
mockCiYml,
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index f0347ad19ac..a26232df58f 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -9,12 +9,12 @@ import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
-import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
-import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.query.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import getLastCommitBranch from '~/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
+import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
+import { DEFAULT_FAILURE } from '~/ci/pipeline_editor/constants';
+import getAvailableBranchesQuery from '~/ci/pipeline_editor/graphql/queries/available_branches.query.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getLastCommitBranch from '~/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import {
mockBranchPaginationLimit,
diff --git a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
index d503aff40b8..907db16913c 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
@@ -3,15 +3,15 @@ import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
-import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
-import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import FileTreePopover from '~/ci/pipeline_editor/components/popovers/file_tree_popover.vue';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
index f79074f1e0f..11ba517e0eb 100644
--- a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { createMockDirective } from 'helpers/vue_mock_directive';
-import PipelineEditorFileTreeContainer from '~/pipeline_editor/components/file_tree/container.vue';
-import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue';
-import { FILE_TREE_TIP_DISMISSED_KEY } from '~/pipeline_editor/constants';
+import PipelineEditorFileTreeContainer from '~/ci/pipeline_editor/components/file_tree/container.vue';
+import PipelineEditorFileTreeItem from '~/ci/pipeline_editor/components/file_tree/file_item.vue';
+import { FILE_TREE_TIP_DISMISSED_KEY } from '~/ci/pipeline_editor/constants';
import { mockCiConfigPath, mockIncludes, mockIncludesHelpPagePath } from '../../mock_data';
describe('Pipeline editor file nav', () => {
diff --git a/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
index f12ac14c6be..bceb741f91c 100644
--- a/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
@@ -1,7 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue';
+import PipelineEditorFileTreeItem from '~/ci/pipeline_editor/components/file_tree/file_item.vue';
import { mockIncludesWithBlob, mockDefaultIncludes } from '../../mock_data';
describe('Pipeline editor file nav', () => {
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
index e1dc08b637f..555b9f29fbf 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
-import PipelineStatus from '~/pipeline_editor/components/header/pipeline_status.vue';
-import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue';
+import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
+import PipelineStatus from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
+import ValidationSegment from '~/ci/pipeline_editor/components/header/validation_segment.vue';
import { mockCiYml, mockLintResponse } from '../../mock_data';
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index d40a9cc8100..6f28362e478 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -3,10 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
+import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 35315db39f8..a62c51ffb59 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -4,9 +4,9 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
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/pipeline.query.graphql';
-import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineStatus, { i18n } from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
+import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
index d40a9cc8100..6f28362e478 100644
--- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -3,10 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
+import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
index 1ad621e6f45..0853a6f4ca4 100644
--- a/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
@@ -8,8 +8,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
import ValidationSegment, {
i18n,
-} from '~/pipeline_editor/components/header/validation_segment.vue';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import {
CI_CONFIG_STATUS_INVALID,
EDITOR_APP_STATUS_EMPTY,
@@ -17,7 +17,7 @@ import {
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_VALID,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
import {
mergeUnwrappedCiConfig,
mockCiYml,
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
index 7f89eda4dff..d43bdec3a33 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -1,7 +1,7 @@
import { GlTableLite, GlLink } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
import { mockJobs, mockErrors, mockWarnings } from '../../mock_data';
describe('CI Lint Results', () => {
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
index 36052a2e16a..b5e3ea06c2c 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import CiLintWarnings from '~/pipeline_editor/components/lint/ci_lint_warnings.vue';
+import CiLintWarnings from '~/ci/pipeline_editor/components/lint/ci_lint_warnings.vue';
const warnings = ['warning 1', 'warning 2', 'warning 3'];
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 27707f8b01a..70310cbdb10 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -6,11 +6,11 @@ import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
-import CiValidate from '~/pipeline_editor/components/validate/ci_validate.vue';
-import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
-import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
-import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
+import CiConfigMergedPreview from '~/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import CiValidate from '~/ci/pipeline_editor/components/validate/ci_validate.vue';
+import WalkthroughPopover from '~/ci/pipeline_editor/components/popovers/walkthrough_popover.vue';
+import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
+import EditorTab from '~/ci/pipeline_editor/components/ui/editor_tab.vue';
import {
CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
@@ -20,9 +20,9 @@ import {
TAB_QUERY_PARAM,
VALIDATE_TAB,
VALIDATE_TAB_BADGE_DISMISSED_KEY,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
import {
mockBlobContentQueryResponse,
mockCiLintPath,
diff --git a/spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
index 98ce3f6ea40..63ebfc0559d 100644
--- a/spec/frontend/pipeline_editor/components/popovers/file_tree_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
@@ -1,8 +1,8 @@
import { nextTick } from 'vue';
import { GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
-import { FILE_TREE_POPOVER_DISMISSED_KEY } from '~/pipeline_editor/constants';
+import FileTreePopover from '~/ci/pipeline_editor/components/popovers/file_tree_popover.vue';
+import { FILE_TREE_POPOVER_DISMISSED_KEY } from '~/ci/pipeline_editor/constants';
import { mockIncludesHelpPagePath } from '../../mock_data';
describe('FileTreePopover component', () => {
diff --git a/spec/frontend/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
index 97f785a71bc..cf0b974081e 100644
--- a/spec/frontend/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
@@ -1,7 +1,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ValidatePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
-import { VALIDATE_TAB_FEEDBACK_URL } from '~/pipeline_editor/constants';
+import ValidatePopover from '~/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
+import { VALIDATE_TAB_FEEDBACK_URL } from '~/ci/pipeline_editor/constants';
import { mockSimulatePipelineHelpPagePath } from '../../mock_data';
describe('ValidatePopover component', () => {
diff --git a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index b86c82850c5..ca6033f2ff5 100644
--- a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -1,6 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
+import WalkthroughPopover from '~/ci/pipeline_editor/components/popovers/walkthrough_popover.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.config.ignoredElements = ['gl-emoji'];
diff --git a/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
index 44fda2812d8..b22c98e5544 100644
--- a/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import ConfirmDialog from '~/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue';
+import ConfirmDialog from '~/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue';
describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
let beforeUnloadEvent;
diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
index 24f27e8c5fb..a4e7abba7b0 100644
--- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlBadge, GlTabs } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
+import EditorTab from '~/ci/pipeline_editor/components/ui/editor_tab.vue';
const mockContent1 = 'MOCK CONTENT 1';
const mockContent2 = 'MOCK CONTENT 2';
@@ -10,7 +10,7 @@ const MockSourceEditor = {
template: '<div>EDITOR</div>',
};
-describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
+describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
let wrapper;
let mockChildMounted = jest.fn();
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index c76c3460e99..3c68f74af43 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
-import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
describe('Pipeline editor empty state', () => {
let wrapper;
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
index d9ecee31e83..fdb3be5c690 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
@@ -2,9 +2,9 @@ import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
-import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants';
-import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
+import CodeSnippetAlert from '~/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
+import { CODE_SNIPPET_SOURCES } from '~/ci/pipeline_editor/components/code_snippet_alert/constants';
+import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
@@ -13,7 +13,7 @@ import {
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
PIPELINE_FAILURE,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
beforeEach(() => {
setWindowLocation(TEST_HOST);
diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
index 09d4f9736ad..ae25142b455 100644
--- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
@@ -5,12 +5,12 @@ import VueApollo from 'vue-apollo';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
-import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_validate.vue';
-import ValidatePipelinePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
-import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
-import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
-import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
+import CiValidate, { i18n } from '~/ci/pipeline_editor/components/validate/ci_validate.vue';
+import ValidatePipelinePopover from '~/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import lintCIMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
import {
mockBlobContentQueryResponse,
mockCiLintPath,
diff --git a/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap b/spec/frontend/ci/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap
index ee5a3cb288f..75a1354fd29 100644
--- a/spec/frontend/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap
+++ b/spec/frontend/ci/pipeline_editor/graphql/__snapshots__/resolvers_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`~/pipeline_editor/graphql/resolvers Mutation lintCI lint data is as expected 1`] = `
+exports[`~/ci/pipeline_editor/graphql/resolvers Mutation lintCI lint data is as expected 1`] = `
Object {
"__typename": "CiLintContent",
"errors": Array [],
diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
index 76ae96c623a..e54c72a758f 100644
--- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import { mockLintResponse } from '../mock_data';
jest.mock('~/api', () => {
@@ -10,7 +10,7 @@ jest.mock('~/api', () => {
};
});
-describe('~/pipeline_editor/graphql/resolvers', () => {
+describe('~/ci/pipeline_editor/graphql/resolvers', () => {
describe('Mutation', () => {
describe('lintCI', () => {
let mock;
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 2ea580b7b53..176dc24f169 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -1,4 +1,4 @@
-import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
+import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
export const mockProjectNamespace = 'user1';
@@ -119,7 +119,7 @@ export const mockIncludes = [
];
// Mock result of the graphql query at:
-// app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
+// app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.graphql
export const mockCiConfigQueryResponse = {
data: {
ciConfig: {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index 9fe1536d3f5..2246d0bbf7e 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -6,30 +6,30 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
-import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
-import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
-import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
-import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
+import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
+import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
+import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
+import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
import ValidationSegment, {
i18n as validationSegmenti18n,
-} from '~/pipeline_editor/components/header/validation_segment.vue';
+} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
import {
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
COMMIT_FAILURE,
EDITOR_APP_STATUS_LOADING,
-} from '~/pipeline_editor/constants';
-import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
-import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
-import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
-import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/pipeline.query.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
-
-import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
-import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+} from '~/ci/pipeline_editor/constants';
+import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import getCiConfigData from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
+import getTemplate from '~/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
+import getLatestCommitShaQuery from '~/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
+import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+
+import PipelineEditorApp from '~/ci/pipeline_editor/pipeline_editor_app.vue';
+import PipelineEditorHome from '~/ci/pipeline_editor/pipeline_editor_home.vue';
import {
mockCiConfigPath,
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 2b06660c4b3..621e015e825 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -3,14 +3,14 @@ import { nextTick } from 'vue';
import { GlButton, GlDrawer, GlModal } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
-import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
-import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
-import PipelineEditorFileTree from '~/pipeline_editor/components/file_tree/container.vue';
-import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
-import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
-import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
+import CiEditorHeader from '~/ci/pipeline_editor/components/editor/ci_editor_header.vue';
+import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
+import PipelineEditorDrawer from '~/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorFileTree from '~/ci/pipeline_editor/components/file_tree/container.vue';
+import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
+import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
+import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
import {
CREATE_TAB,
FILE_TREE_DISPLAY_KEY,
@@ -18,8 +18,8 @@ import {
MERGED_TAB,
TABS_INDEX,
VISUALIZE_TAB,
-} from '~/pipeline_editor/constants';
-import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+} from '~/ci/pipeline_editor/constants';
+import PipelineEditorHome from '~/ci/pipeline_editor/pipeline_editor_home.vue';
import { mockLintResponse, mockCiYml } from './mock_data';
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index e5d9b378a42..639c2dbef4c 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -1,25 +1,160 @@
-import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import { GlForm } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
import PipelineSchedulesForm from '~/ci/pipeline_schedules/components/pipeline_schedules_form.vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers';
describe('Pipeline schedules form', () => {
let wrapper;
+ const defaultBranch = 'main';
+ const projectId = '1';
+ const cron = '';
+ const dailyLimit = '';
- const createComponent = () => {
- wrapper = shallowMount(PipelineSchedulesForm);
+ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
+ wrapper = mountFn(PipelineSchedulesForm, {
+ propsData: {
+ timezoneData: timezoneDataFixture,
+ refParam: 'master',
+ },
+ provide: {
+ fullPath: 'gitlab-org/gitlab',
+ projectId,
+ defaultBranch,
+ cron,
+ cronTimezone: '',
+ dailyLimit,
+ settingsLink: '',
+ },
+ stubs,
+ });
};
const findForm = () => wrapper.findComponent(GlForm);
+ const findDescription = () => wrapper.findByTestId('schedule-description');
+ const findIntervalComponent = () => wrapper.findComponent(IntervalPatternInput);
+ const findTimezoneDropdown = () => wrapper.findComponent(TimezoneDropdown);
+ const findRefSelector = () => wrapper.findComponent(RefSelector);
+ const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button');
+ const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button');
+ // Variables
+ const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
+ const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
+ const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
+ const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
+ describe('Form elements', () => {
+ it('displays form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('displays the description input', () => {
+ expect(findDescription().exists()).toBe(true);
+ });
+
+ it('displays the interval pattern component', () => {
+ const intervalPattern = findIntervalComponent();
+
+ expect(intervalPattern.exists()).toBe(true);
+ expect(intervalPattern.props()).toMatchObject({
+ initialCronInterval: cron,
+ dailyLimit,
+ sendNativeErrors: false,
+ });
+ });
+
+ it('displays the Timezone dropdown', () => {
+ const timezoneDropdown = findTimezoneDropdown();
+
+ expect(timezoneDropdown.exists()).toBe(true);
+ expect(timezoneDropdown.props()).toMatchObject({
+ value: '',
+ name: 'schedule-timezone',
+ timezoneData: timezoneDataFixture,
+ });
+ });
+
+ it('displays the branch/tag selector', () => {
+ const refSelector = findRefSelector();
+
+ expect(refSelector.exists()).toBe(true);
+ expect(refSelector.props()).toMatchObject({
+ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
+ value: defaultBranch,
+ projectId,
+ translations: { dropdownHeader: 'Select target branch or tag' },
+ useSymbolicRefNames: true,
+ state: true,
+ name: '',
+ });
+ });
+
+ it('displays the submit and cancel buttons', () => {
+ expect(findSubmitButton().exists()).toBe(true);
+ expect(findCancelButton().exists()).toBe(true);
+ });
});
- it('displays form', () => {
- expect(findForm().exists()).toBe(true);
+ describe('CI variables', () => {
+ let mock;
+
+ const addVariableToForm = () => {
+ const input = findKeyInputs().at(0);
+ input.element.value = 'test_var_2';
+ input.trigger('change');
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent(mountExtended);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('creates blank variable on input change event', async () => {
+ expect(findVariableRows()).toHaveLength(1);
+
+ addVariableToForm();
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(2);
+ expect(findKeyInputs().at(1).element.value).toBe('');
+ expect(findValueInputs().at(1).element.value).toBe('');
+ });
+
+ it('does not display remove icon for last row', async () => {
+ addVariableToForm();
+
+ await nextTick();
+
+ expect(findRemoveIcons()).toHaveLength(1);
+ });
+
+ it('removes ci variable row on remove icon button click', async () => {
+ addVariableToForm();
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(2);
+
+ findRemoveIcons().at(0).trigger('click');
+
+ await nextTick();
+
+ expect(findVariableRows()).toHaveLength(1);
+ });
});
});
diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index c32b52d9e77..5ca4b25da9b 100644
--- a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -1,8 +1,8 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import component from '~/reports/codequality_report/components/codequality_issue_body.vue';
-import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import component from '~/ci/reports/codequality_report/components/codequality_issue_body.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants';
describe('code quality issue body issue body', () => {
let wrapper;
diff --git a/spec/frontend/reports/codequality_report/mock_data.js b/spec/frontend/ci/reports/codequality_report/mock_data.js
index 2c994116db6..2c994116db6 100644
--- a/spec/frontend/reports/codequality_report/mock_data.js
+++ b/spec/frontend/ci/reports/codequality_report/mock_data.js
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
index 1878b9f44b2..88628210793 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
@@ -2,10 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
-import createStore from '~/reports/codequality_report/store';
-import * as actions from '~/reports/codequality_report/store/actions';
-import * as types from '~/reports/codequality_report/store/mutation_types';
-import { STATUS_NOT_FOUND } from '~/reports/constants';
+import createStore from '~/ci/reports/codequality_report/store';
+import * as actions from '~/ci/reports/codequality_report/store/actions';
+import * as types from '~/ci/reports/codequality_report/store/mutation_types';
+import { STATUS_NOT_FOUND } from '~/ci/reports/constants';
import { reportIssues, parsedReportIssues } from '../mock_data';
const pollInterval = 123;
diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/ci/reports/codequality_report/store/getters_spec.js
index 646903390ff..f4505204f67 100644
--- a/spec/frontend/reports/codequality_report/store/getters_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/getters_spec.js
@@ -1,6 +1,6 @@
-import createStore from '~/reports/codequality_report/store';
-import * as getters from '~/reports/codequality_report/store/getters';
-import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '~/reports/constants';
+import createStore from '~/ci/reports/codequality_report/store';
+import * as getters from '~/ci/reports/codequality_report/store/getters';
+import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '~/ci/reports/constants';
describe('Codequality reports store getters', () => {
let localState;
diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/ci/reports/codequality_report/store/mutations_spec.js
index 6e14cd7438b..22ff86b1040 100644
--- a/spec/frontend/reports/codequality_report/store/mutations_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import createStore from '~/reports/codequality_report/store';
-import mutations from '~/reports/codequality_report/store/mutations';
-import { STATUS_NOT_FOUND } from '~/reports/constants';
+import createStore from '~/ci/reports/codequality_report/store';
+import mutations from '~/ci/reports/codequality_report/store/mutations';
+import { STATUS_NOT_FOUND } from '~/ci/reports/constants';
describe('Codequality Reports mutations', () => {
let localState;
diff --git a/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js b/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js
index 5b77a2c74be..f7d82d2b662 100644
--- a/spec/frontend/reports/codequality_report/store/utils/codequality_parser_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js
@@ -1,5 +1,5 @@
-import { reportIssues, parsedReportIssues } from 'jest/reports/codequality_report/mock_data';
-import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
+import { reportIssues, parsedReportIssues } from 'jest/ci/reports/codequality_report/mock_data';
+import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
describe('Codequality report store utils', () => {
let result;
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
index 311a67a3e31..311a67a3e31 100644
--- a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
diff --git a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
index b5a4cb42463..b5a4cb42463 100644
--- a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
+++ b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
index cacbde590d6..3e4adfc7794 100644
--- a/spec/frontend/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
-import ReportItem from '~/reports/components/report_item.vue';
+import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Grouped Issues List', () => {
diff --git a/spec/frontend/reports/components/issue_status_icon_spec.js b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
index 8706f2f8d83..fb13d4407e2 100644
--- a/spec/frontend/reports/components/issue_status_icon_spec.js
+++ b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import ReportItem from '~/reports/components/issue_status_icon.vue';
-import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import ReportItem from '~/ci/reports/components/issue_status_icon.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants';
describe('IssueStatusIcon', () => {
let wrapper;
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/ci/reports/components/report_item_spec.js
index 60c7e5f2b44..d835d549531 100644
--- a/spec/frontend/reports/components/report_item_spec.js
+++ b/spec/frontend/ci/reports/components/report_item_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import { componentNames } from '~/reports/components/issue_body';
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
-import ReportItem from '~/reports/components/report_item.vue';
-import { STATUS_SUCCESS } from '~/reports/constants';
+import { componentNames } from '~/ci/reports/components/issue_body';
+import IssueStatusIcon from '~/ci/reports/components/issue_status_icon.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import { STATUS_SUCCESS } from '~/ci/reports/constants';
describe('ReportItem', () => {
describe('showReportSectionStatusIcon', () => {
diff --git a/spec/frontend/reports/components/report_link_spec.js b/spec/frontend/ci/reports/components/report_link_spec.js
index 2ed0617a598..ba541ba0303 100644
--- a/spec/frontend/reports/components/report_link_spec.js
+++ b/spec/frontend/ci/reports/components/report_link_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import ReportLink from '~/reports/components/report_link.vue';
+import ReportLink from '~/ci/reports/components/report_link.vue';
-describe('app/assets/javascripts/reports/components/report_link.vue', () => {
+describe('app/assets/javascripts/ci/reports/components/report_link.vue', () => {
let wrapper;
afterEach(() => {
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/ci/reports/components/report_section_spec.js
index cc35b99a199..f032b210184 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/ci/reports/components/report_section_spec.js
@@ -1,8 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ReportItem from '~/reports/components/report_item.vue';
-import ReportSection from '~/reports/components/report_section.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import ReportSection from '~/ci/reports/components/report_section.vue';
describe('ReportSection component', () => {
let wrapper;
diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
index 778660d9e44..fb2ae5371d5 100644
--- a/spec/frontend/reports/components/summary_row_spec.js
+++ b/spec/frontend/ci/reports/components/summary_row_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import SummaryRow from '~/reports/components/summary_row.vue';
+import SummaryRow from '~/ci/reports/components/summary_row.vue';
describe('Summary row', () => {
let wrapper;
diff --git a/spec/frontend/reports/mock_data/mock_data.js b/spec/frontend/ci/reports/mock_data/mock_data.js
index 2599b0ac365..2599b0ac365 100644
--- a/spec/frontend/reports/mock_data/mock_data.js
+++ b/spec/frontend/ci/reports/mock_data/mock_data.js
diff --git a/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json b/spec/frontend/ci/reports/mock_data/new_and_fixed_failures_report.json
index 6141e5433a6..9018ad5e4cf 100644
--- a/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_and_fixed_failures_report.json
@@ -1,11 +1,21 @@
{
"status": "failed",
- "summary": { "total": 11, "resolved": 2, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 2,
+ "errored": 0,
+ "failed": 2
+ },
"suites": [
{
"name": "rspec:pg",
"status": "failed",
- "summary": { "total": 8, "resolved": 2, "errored": 0, "failed": 1 },
+ "summary": {
+ "total": 8,
+ "resolved": 2,
+ "errored": 0,
+ "failed": 1
+ },
"new_failures": [
{
"status": "failed",
@@ -36,7 +46,12 @@
{
"name": "java ant",
"status": "failed",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 1 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 1
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [
@@ -52,4 +67,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/new_errors_report.json b/spec/frontend/ci/reports/mock_data/new_errors_report.json
index 6573d23ee50..d3fb570c327 100644
--- a/spec/frontend/reports/mock_data/new_errors_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_errors_report.json
@@ -1,9 +1,19 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 2, "failed": 0 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 2,
+ "failed": 0
+ },
"suites": [
{
"name": "karma",
- "summary": { "total": 3, "resolved": 0, "errored": 2, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 2,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -26,7 +36,12 @@
},
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -35,4 +50,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/new_failures_report.json b/spec/frontend/ci/reports/mock_data/new_failures_report.json
index 438f7c82788..03a875b7636 100644
--- a/spec/frontend/reports/mock_data/new_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_failures_report.json
@@ -1,9 +1,19 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"suites": [
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"new_failures": [
{
"result": "failure",
@@ -28,7 +38,12 @@
},
{
"name": "java ant",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -37,4 +52,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json b/spec/frontend/ci/reports/mock_data/new_failures_with_null_files_report.json
index 28ee7d194b9..00a35a3d0a7 100644
--- a/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json
+++ b/spec/frontend/ci/reports/mock_data/new_failures_with_null_files_report.json
@@ -1,9 +1,19 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"suites": [
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2
+ },
"new_failures": [
{
"result": "failure",
@@ -28,7 +38,12 @@
},
{
"name": "java ant",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -37,4 +52,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/no_failures_report.json b/spec/frontend/ci/reports/mock_data/no_failures_report.json
index 7da9e0c6211..a48a206208d 100644
--- a/spec/frontend/reports/mock_data/no_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/no_failures_report.json
@@ -1,11 +1,21 @@
{
"status": "success",
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"suites": [
{
"name": "rspec:pg",
"status": "success",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -16,7 +26,12 @@
{
"name": "java ant",
"status": "success",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -25,4 +40,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/recent_failures_report.json b/spec/frontend/ci/reports/mock_data/recent_failures_report.json
index c4a5fb78dcd..f4fc2d2e927 100644
--- a/spec/frontend/reports/mock_data/recent_failures_report.json
+++ b/spec/frontend/ci/reports/mock_data/recent_failures_report.json
@@ -1,9 +1,21 @@
{
- "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 3, "recentlyFailed": 2 },
+ "summary": {
+ "total": 11,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 3,
+ "recentlyFailed": 2
+ },
"suites": [
{
"name": "rspec:pg",
- "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2, "recentlyFailed": 1 },
+ "summary": {
+ "total": 8,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 2,
+ "recentlyFailed": 1
+ },
"new_failures": [
{
"result": "failure",
@@ -30,7 +42,13 @@
},
{
"name": "java ant",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 1, "recentlyFailed": 1 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 1,
+ "recentlyFailed": 1
+ },
"new_failures": [
{
"result": "failure",
@@ -49,4 +67,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/reports/mock_data/resolved_failures.json b/spec/frontend/ci/reports/mock_data/resolved_failures.json
index 49de6aa840b..15012fb027d 100644
--- a/spec/frontend/reports/mock_data/resolved_failures.json
+++ b/spec/frontend/ci/reports/mock_data/resolved_failures.json
@@ -1,11 +1,21 @@
{
"status": "success",
- "summary": { "total": 11, "resolved": 4, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 11,
+ "resolved": 4,
+ "errored": 0,
+ "failed": 0
+ },
"suites": [
{
"name": "rspec:pg",
"status": "success",
- "summary": { "total": 8, "resolved": 4, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 8,
+ "resolved": 4,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [
{
@@ -18,7 +28,7 @@
{
"status": "success",
"name": "Test#sum when a is 100 and b is 200 returns summary",
- "execution_time": 7.6e-5,
+ "execution_time": 0.000076,
"system_output": null,
"stack_trace": null
}
@@ -46,7 +56,12 @@
{
"name": "java ant",
"status": "success",
- "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 },
+ "summary": {
+ "total": 3,
+ "resolved": 0,
+ "errored": 0,
+ "failed": 0
+ },
"new_failures": [],
"resolved_failures": [],
"existing_failures": [],
@@ -55,4 +70,4 @@
"existing_errors": []
}
]
-}
+} \ No newline at end of file
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index 7081bc57467..e233268b756 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
import { GlTab, GlTabs } from '@gitlab/ui';
+import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -33,12 +35,15 @@ const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
const mockRunnersPath = '/admin/runners';
Vue.use(VueApollo);
+Vue.use(VueRouter);
describe('AdminRunnerShowApp', () => {
let wrapper;
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findTabAt = (i) => wrapper.findAllComponents(GlTab).at(i);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
@@ -113,6 +118,16 @@ describe('AdminRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
+ it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
+ setWindowLocation(hash);
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findTabs().props('value')).toBe(0);
+ expect(findRunnerDetails().exists()).toBe(true);
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
+
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
@@ -226,7 +241,7 @@ describe('AdminRunnerShowApp', () => {
});
});
- describe('Jobs tab', () => {
+ describe('When showing jobs', () => {
const stubs = {
GlTab,
GlTabs,
@@ -245,6 +260,17 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnersJobs().exists()).toBe(false);
});
+ it('when URL hash links to jobs tab', async () => {
+ mockRunnerQueryResult();
+ setWindowLocation('#/jobs');
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findTabs().props('value')).toBe(1);
+ expect(findRunnerDetails().exists()).toBe(false);
+ expect(findRunnersJobs().exists()).toBe(true);
+ });
+
it('without a job count, shows no jobs count', async () => {
mockRunnerQueryResult({ jobCount: null });
@@ -260,7 +286,28 @@ describe('AdminRunnerShowApp', () => {
await createComponent({ stubs });
expect(findJobCountBadge().text()).toBe('3');
- expect(findRunnersJobs().props('runner')).toEqual({ ...mockRunner, ...runner });
+ });
+ });
+
+ describe('When navigating to another tab', () => {
+ let routerPush;
+
+ beforeEach(async () => {
+ mockRunnerQueryResult();
+
+ await createComponent({ mountFn: mountExtended });
+
+ routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
+ });
+
+ it('navigates to details', () => {
+ findTabAt(0).vm.$emit('click');
+ expect(routerPush).toHaveBeenLastCalledWith({ name: 'details' });
+ });
+
+ it('navigates to job', () => {
+ findTabAt(1).vm.$emit('click');
+ expect(routerPush).toHaveBeenLastCalledWith({ name: 'jobs' });
});
});
});
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 9778a6fe66c..9084ecdb4cc 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -25,6 +25,7 @@ import RunnerStats from '~/ci/runner/components/stat/runner_stats.vue';
import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue';
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
@@ -77,7 +78,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
Vue.use(VueApollo);
Vue.use(GlToast);
-const COUNT_QUERIES = 7; // 4 tabs + 3 status queries
+const STATUS_COUNT_QUERIES = 3;
+const TAB_COUNT_QUERIES = 4;
+const COUNT_QUERIES = TAB_COUNT_QUERIES + STATUS_COUNT_QUERIES;
describe('AdminRunnersApp', () => {
let wrapper;
@@ -170,6 +173,29 @@ describe('AdminRunnersApp', () => {
});
});
+ describe('does not show total runner counts when total is 0', () => {
+ beforeEach(async () => {
+ mockRunnersCountHandler.mockResolvedValue({
+ data: {
+ runners: {
+ count: 0,
+ ...runnersCountData.runners,
+ },
+ },
+ });
+
+ await createComponent({ mountFn: mountExtended });
+ });
+
+ it('fetches only tab counts', () => {
+ expect(mockRunnersCountHandler).toHaveBeenCalledTimes(TAB_COUNT_QUERIES);
+ });
+
+ it('does not shows counters', () => {
+ expect(findRunnerStats().text()).toBe('');
+ });
+ });
+
it('shows the runners list', async () => {
await createComponent();
@@ -252,6 +278,15 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
+ it('Shows job status and links to jobs', () => {
+ const badge = wrapper
+ .find('tr [data-testid="td-summary"]')
+ .findComponent(RunnerJobStatusBadge);
+
+ expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
+ expect(badge.attributes('href')).toBe(`http://localhost/admin/runners/${id}#/jobs`);
+ });
+
it('When runner is paused or unpaused, some data is refetched', async () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
diff --git a/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 4aa354f9b62..10280c77303 100644
--- a/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -1,12 +1,18 @@
import { __ } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerStackedSummaryCell from '~/ci/runner/components/cells/runner_stacked_summary_cell.vue';
+import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+import {
+ INSTANCE_TYPE,
+ I18N_INSTANCE_TYPE,
+ PROJECT_TYPE,
+ I18N_NO_DESCRIPTION,
+} from '~/ci/runner/constants';
import { allRunnersData } from '../../mock_data';
@@ -16,13 +22,14 @@ describe('RunnerTypeCell', () => {
let wrapper;
const findLockIcon = () => wrapper.findByTestId('lock-icon');
+ const findRunnerJobStatusBadge = () => wrapper.findComponent(RunnerJobStatusBadge);
const findRunnerTags = () => wrapper.findComponent(RunnerTags);
const findRunnerSummaryField = (icon) =>
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
.wrappers[0];
const createComponent = (runner, options) => {
- wrapper = mountExtended(RunnerStackedSummaryCell, {
+ wrapper = mountExtended(RunnerSummaryCell, {
propsData: {
runner: {
...mockRunner,
@@ -80,6 +87,18 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(mockRunner.description);
});
+ it('Displays the no runner description', () => {
+ createComponent({
+ description: null,
+ });
+
+ expect(wrapper.text()).toContain(I18N_NO_DESCRIPTION);
+ });
+
+ it('Displays job execution status', () => {
+ expect(findRunnerJobStatusBadge().props('jobStatus')).toBe(mockRunner.jobExecutionStatus);
+ });
+
it('Displays last contact', () => {
createComponent({
contactedAt: '2022-01-02',
@@ -147,14 +166,14 @@ describe('RunnerTypeCell', () => {
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
});
- it('Displays a custom slot', () => {
+ it.each(['runner-name', 'runner-job-status-badge'])('Displays a custom "%s" slot', (slotName) => {
const slotContent = 'My custom runner name';
createComponent(
{},
{
slots: {
- 'runner-name': slotContent,
+ [slotName]: slotContent,
},
},
);
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index 496c144083e..408750e646f 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -13,6 +13,7 @@ import {
DEFAULT_SORT,
CONTACTED_DESC,
} from '~/ci/runner/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -34,7 +35,7 @@ describe('RunnerList', () => {
const mockOtherSort = CONTACTED_DESC;
const mockFilters = [
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
];
const expectToHaveLastEmittedInput = (value) => {
diff --git a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
new file mode 100644
index 00000000000..015bebf40e3
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
@@ -0,0 +1,51 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
+import {
+ I18N_JOB_STATUS_RUNNING,
+ I18N_JOB_STATUS_IDLE,
+ JOB_STATUS_RUNNING,
+ JOB_STATUS_IDLE,
+} from '~/ci/runner/constants';
+
+describe('RunnerTypeBadge', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
+ const createComponent = ({ props, ...options } = {}) => {
+ wrapper = shallowMount(RunnerJobStatusBadge, {
+ propsData: {
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ it.each`
+ jobStatus | classes | text
+ ${JOB_STATUS_RUNNING} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-blue-600!', 'gl-border', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
+ ${JOB_STATUS_IDLE} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-gray-700!', 'gl-border', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE}
+ `(
+ 'renders $jobStatus job status with "$text" text and styles',
+ ({ jobStatus, classes, text }) => {
+ createComponent({ props: { jobStatus } });
+
+ expect(findBadge().props()).toMatchObject({ size: 'sm', variant: 'muted' });
+ expect(findBadge().classes().sort()).toEqual(classes.sort());
+ expect(findBadge().text()).toBe(text);
+ },
+ );
+
+ it('does not render an unknown status', () => {
+ createComponent({ props: { jobStatus: 'UNKNOWN_STATUS' } });
+
+ expect(wrapper.html()).toBe('');
+ });
+
+ it('adds arbitrary attributes', () => {
+ createComponent({ props: { jobStatus: JOB_STATUS_RUNNING }, attrs: { href: '/url' } });
+
+ expect(findBadge().attributes('href')).toBe('/url');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index d53a0ce8f4f..1267d045623 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -188,6 +188,21 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
});
+ it('Render #runner-job-status-badge slot in "summary" cell', () => {
+ createComponent(
+ {
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
+ },
+ mountExtended,
+ );
+
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(
+ `Job status ${mockRunners[0].jobExecutionStatus}`,
+ );
+ });
+
it('Render #runner-actions-cell slot in "actions" cell', () => {
createComponent(
{
diff --git a/spec/frontend/ci/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
index 7d3064c2aef..45b410df2d4 100644
--- a/spec/frontend/ci/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
@@ -37,12 +37,12 @@ describe('RunnerTypeBadge', () => {
};
beforeEach(() => {
- jest.useFakeTimers('modern');
+ jest.useFakeTimers({ legacyFakeTimers: false });
jest.setSystemTime(new Date('2021-01-01T00:00:00Z'));
});
afterEach(() => {
- jest.useFakeTimers('legacy');
+ jest.useFakeTimers({ legacyFakeTimers: true });
wrapper.destroy();
});
diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index d3c7ea50f9d..3dce5a509ca 100644
--- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -7,7 +7,7 @@ import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
jest.mock('~/flash');
@@ -42,7 +42,7 @@ const mockTagTokenConfig = {
type: 'tag',
token: TagToken,
recentSuggestionsStorageKey: mockStorageKey,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
describe('TagToken', () => {
diff --git a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
index daebf3df050..3d45674d106 100644
--- a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
@@ -16,6 +16,23 @@ describe('RunnerStats', () => {
const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat);
+ const RunnerCountStub = {
+ props: ['variables'],
+ render() {
+ // return a count for each status
+ const mockCounts = {
+ undefined: 6, // no status returns "all"
+ [STATUS_ONLINE]: 3,
+ [STATUS_OFFLINE]: 2,
+ [STATUS_STALE]: 1,
+ };
+
+ return this.$scopedSlots.default({
+ count: mockCounts[this.variables.status],
+ });
+ },
+ };
+
const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(RunnerStats, {
propsData: {
@@ -23,6 +40,9 @@ describe('RunnerStats', () => {
variables: {},
...props,
},
+ stubs: {
+ RunnerCount: RunnerCountStub,
+ },
...options,
});
};
@@ -32,24 +52,8 @@ describe('RunnerStats', () => {
});
it('Displays all the stats', () => {
- const mockCounts = {
- [STATUS_ONLINE]: 3,
- [STATUS_OFFLINE]: 2,
- [STATUS_STALE]: 1,
- };
-
createComponent({
mountFn: mount,
- stubs: {
- RunnerCount: {
- props: ['variables'],
- render() {
- return this.$scopedSlots.default({
- count: mockCounts[this.variables.status],
- });
- },
- },
- },
});
const text = wrapper.text();
@@ -78,4 +82,21 @@ describe('RunnerStats', () => {
expect(stat.props('variables')).toMatchObject(mockVariables);
});
});
+
+ it('Does not display counts when total is 0', () => {
+ createComponent({
+ mountFn: mount,
+ stubs: {
+ RunnerCount: {
+ render() {
+ return this.$scopedSlots.default({
+ count: 0,
+ });
+ },
+ },
+ },
+ });
+
+ expect(wrapper.html()).toBe('');
+ });
});
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index c3493b3c9fd..1e5bb828dbf 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -448,13 +448,15 @@ describe('GroupRunnersApp', () => {
it('navigates to the next page', async () => {
await findRunnerPaginationNext().trigger('click');
- expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({
- groupFullPath: mockGroupFullPath,
- membership: MEMBERSHIP_DESCENDANTS,
- sort: CREATED_DESC,
- first: RUNNER_PAGE_SIZE,
- after: pageInfo.endCursor,
- });
+ expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ groupFullPath: mockGroupFullPath,
+ membership: MEMBERSHIP_DESCENDANTS,
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ after: pageInfo.endCursor,
+ }),
+ );
});
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index eff5abc21b5..525756ed513 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -18,6 +18,7 @@ import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/grou
import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/ci/runner/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
const emptyPageInfo = {
__typename: 'PageInfo',
@@ -73,7 +74,7 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'something' },
},
],
@@ -95,11 +96,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'something' },
},
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'else' },
},
],
diff --git a/spec/frontend/ci/runner/runner_search_utils_spec.js b/spec/frontend/ci/runner/runner_search_utils_spec.js
index 1db8fa1829b..f64b89d47fd 100644
--- a/spec/frontend/ci/runner/runner_search_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_search_utils_spec.js
@@ -6,6 +6,7 @@ import {
fromSearchToVariables,
isSearchFiltered,
} from 'ee_else_ce/ci/runner/runner_search_utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import { mockSearchExamples } from './mock_data';
describe('search_params.js', () => {
@@ -48,8 +49,8 @@ describe('search_params.js', () => {
it('When search params appear as array, they are concatenated', () => {
expect(fromUrlQueryToSearch('?search[]=my&search[]=text').filters).toEqual([
- { type: 'filtered-search-term', value: { data: 'my' } },
- { type: 'filtered-search-term', value: { data: 'text' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'my' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'text' } },
]);
});
});
@@ -64,12 +65,13 @@ describe('search_params.js', () => {
it.each([
'http://test.host/?status[]=ACTIVE',
'http://test.host/?runner_type[]=INSTANCE_TYPE',
+ 'http://test.host/?paused[]=true',
'http://test.host/?search=my_text',
- ])('When a filter is removed, it is removed from the URL', (initalUrl) => {
+ ])('When a filter is removed, it is removed from the URL', (initialUrl) => {
const search = { filters: [], sort: 'CREATED_DESC' };
const expectedUrl = `http://test.host/`;
- expect(fromSearchToUrl(search, initalUrl)).toBe(expectedUrl);
+ expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
});
it('When unrelated search parameter is present, it does not get removed', () => {
@@ -93,7 +95,7 @@ describe('search_params.js', () => {
fromSearchToVariables({
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: '' },
},
],
@@ -106,11 +108,11 @@ describe('search_params.js', () => {
fromSearchToVariables({
filters: [
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'something' },
},
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: '' },
},
],
diff --git a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
index c7375acd8e5..aa83638773d 100644
--- a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
@@ -24,6 +24,7 @@ describe('Ci Project Variable wrapper', () => {
expect(findCiShared().props()).toEqual({
areScopedVariablesAvailable: false,
componentName: 'InstanceVariables',
+ entity: '',
hideEnvironmentScope: true,
mutationData: wrapper.vm.$options.mutationData,
queryData: wrapper.vm.$options.queryData,
diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
index ef5a86ccb61..ef624d8e4b4 100644
--- a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
@@ -39,6 +39,7 @@ describe('Ci Group Variable wrapper', () => {
id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId),
areScopedVariablesAvailable: false,
componentName: 'GroupVariables',
+ entity: 'group',
fullPath: mockProvide.groupPath,
hideEnvironmentScope: false,
mutationData: wrapper.vm.$options.mutationData,
diff --git a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
index 97051325f59..53c25e430f2 100644
--- a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
@@ -35,6 +35,7 @@ describe('Ci Project Variable wrapper', () => {
id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId),
areScopedVariablesAvailable: true,
componentName: 'ProjectVariables',
+ entity: 'project',
fullPath: mockProvide.projectFullPath,
hideEnvironmentScope: false,
mutationData: wrapper.vm.$options.mutationData,
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index e4771f040d1..d177e755591 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -68,6 +68,7 @@ describe('Ci variable modal', () => {
findModal()
.findAllComponents(GlButton)
.wrappers.find((button) => button.props('variant') === 'danger');
+ const findExpandedVariableCheckbox = () => wrapper.findByTestId('ci-variable-expanded-checkbox');
const findProtectedVariableCheckbox = () =>
wrapper.findByTestId('ci-variable-protected-checkbox');
const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox');
@@ -75,6 +76,7 @@ describe('Ci variable modal', () => {
const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link');
const findEnvScopeInput = () =>
wrapper.findByTestId('environment-scope').findComponent(GlFormInput);
+ const findRawVarTip = () => wrapper.findByTestId('raw-variable-tip');
const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
@@ -188,7 +190,7 @@ describe('Ci variable modal', () => {
});
});
- describe('Reference warning when adding a variable', () => {
+ describe('when expanded', () => {
describe('with a $ character', () => {
beforeEach(() => {
const [variable] = mockVariables;
@@ -205,6 +207,10 @@ describe('Ci variable modal', () => {
it(`renders the variable reference warning`, () => {
expect(findReferenceWarning().exists()).toBe(true);
});
+
+ it(`does not render raw variable tip`, () => {
+ expect(findRawVarTip().exists()).toBe(false);
+ });
});
describe('without a $ character', () => {
@@ -219,6 +225,73 @@ describe('Ci variable modal', () => {
it(`does not render the variable reference warning`, () => {
expect(findReferenceWarning().exists()).toBe(false);
});
+
+ it(`does not render raw variable tip`, () => {
+ expect(findRawVarTip().exists()).toBe(false);
+ });
+ });
+
+ describe('setting raw value', () => {
+ const [variable] = mockVariables;
+
+ it('defaults to expanded and raw:false when adding a variable', () => {
+ createComponent({ props: { selectedVariable: variable } });
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findModal().vm.$emit('shown');
+
+ expect(findExpandedVariableCheckbox().attributes('checked')).toBe('true');
+
+ findAddorUpdateButton().vm.$emit('click');
+
+ expect(wrapper.emitted('add-variable')).toEqual([
+ [
+ {
+ ...variable,
+ raw: false,
+ },
+ ],
+ ]);
+ });
+
+ it('sets correct raw value when editing', async () => {
+ createComponent({
+ props: {
+ selectedVariable: variable,
+ mode: EDIT_VARIABLE_ACTION,
+ },
+ });
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findModal().vm.$emit('shown');
+ await findExpandedVariableCheckbox().vm.$emit('change');
+ await findAddorUpdateButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-variable')).toEqual([
+ [
+ {
+ ...variable,
+ raw: true,
+ },
+ ],
+ ]);
+ });
+ });
+ });
+
+ describe('when not expanded', () => {
+ describe('with a $ character', () => {
+ beforeEach(() => {
+ const selectedVariable = mockVariables[1];
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable },
+ });
+ });
+
+ it(`renders raw variable tip`, () => {
+ expect(findRawVarTip().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
index 8b5a0f7ae9d..5e459ee390f 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
@@ -17,40 +17,45 @@ describe('Ci variable table', () => {
const defaultProps = {
areScopedVariablesAvailable: true,
+ entity: 'project',
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
+ maxVariableLimit: 5,
variables: mockVariablesWithScopes(projectString),
};
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
const findCiVariableModal = () => wrapper.findComponent(ciVariableModal);
- const createComponent = () => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CiVariableSettings, {
propsData: {
...defaultProps,
+ ...props,
},
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
describe('props passing', () => {
it('passes props down correctly to the ci table', () => {
+ createComponent();
+
expect(findCiVariableTable().props()).toEqual({
+ entity: 'project',
isLoading: defaultProps.isLoading,
+ maxVariableLimit: defaultProps.maxVariableLimit,
variables: defaultProps.variables,
});
});
it('passes props down correctly to the ci modal', async () => {
+ createComponent();
+
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
@@ -66,6 +71,10 @@ describe('Ci variable table', () => {
});
describe('modal mode', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('passes down ADD mode when receiving an empty variable', async () => {
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
@@ -82,6 +91,10 @@ describe('Ci variable table', () => {
});
describe('variable modal', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('is hidden by default', () => {
expect(findCiVariableModal().exists()).toBe(false);
});
@@ -112,6 +125,10 @@ describe('Ci variable table', () => {
});
describe('variable events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it.each`
eventName
${'add-variable'}
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
index 0cc0ee7a9c7..65a58a1647f 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
@@ -29,6 +29,8 @@ import {
createGroupProps,
createInstanceProps,
createProjectProps,
+ createGroupProvide,
+ createProjectProvide,
devName,
mockProjectEnvironments,
mockProjectVariables,
@@ -44,6 +46,8 @@ Vue.use(VueApollo);
const mockProvide = {
endpoint: '/variables',
+ isGroup: false,
+ isProject: false,
};
const defaultProps = {
@@ -68,6 +72,7 @@ describe('Ci Variable Shared Component', () => {
customHandlers = null,
isLoading = false,
props = { ...createProjectProps() },
+ provide = {},
} = {}) {
const handlers = customHandlers || [
[getProjectEnvironments, mockEnvironments],
@@ -81,7 +86,10 @@ describe('Ci Variable Shared Component', () => {
...defaultProps,
...props,
},
- provide: mockProvide,
+ provide: {
+ ...mockProvide,
+ ...provide,
+ },
apolloProvider: mockApollo,
stubs: { ciVariableSettings, ciVariableTable },
});
@@ -108,12 +116,18 @@ describe('Ci Variable Shared Component', () => {
});
describe('when queries are resolved', () => {
- describe('successfuly', () => {
+ describe('successfully', () => {
beforeEach(async () => {
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo();
+ await createComponentWithApollo({ provide: createProjectProvide() });
+ });
+
+ it('passes down the expected max variable limit as props', () => {
+ expect(findCiSettings().props('maxVariableLimit')).toBe(
+ mockProjectVariables.data.project.ciVariables.limit,
+ );
});
it('passes down the expected environments as props', () => {
@@ -285,23 +299,29 @@ describe('Ci Variable Shared Component', () => {
});
describe('Props', () => {
+ const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
+ const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
+
describe('in a specific context as', () => {
it.each`
- name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | mutation
- ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${null}
- ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${getGroupVariables}
- ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${getAdminVariables}
+ name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
+ ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
+ ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
+ ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
`(
'passes down all the required props when its a $name component',
async ({
mutation,
+ maxVariableLimit,
mockVariablesValue,
mockEnvironmentsValue,
withEnvironments,
expectedEnvironments,
propsFn,
+ provideFn,
}) => {
const props = propsFn();
+ const provide = provideFn();
mockVariables.mockResolvedValue(mockVariablesValue);
@@ -315,13 +335,15 @@ describe('Ci Variable Shared Component', () => {
customHandlers = [[mutation, mockVariables]];
}
- await createComponentWithApollo({ customHandlers, props });
+ await createComponentWithApollo({ customHandlers, props, provide });
expect(findCiSettings().props()).toEqual({
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
isLoading: false,
+ maxVariableLimit,
variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes,
+ entity: props.entity,
environments: expectedEnvironments,
});
},
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
index 8a4c35173ec..9891bc397b6 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
@@ -1,16 +1,22 @@
+import { GlAlert } from '@gitlab/ui';
+import { sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import { projectString } from '~/ci_variable_list/constants';
+import { EXCEEDS_VARIABLE_LIMIT_TEXT, projectString } from '~/ci_variable_list/constants';
import { mockVariables } from '../mocks';
describe('Ci variable table', () => {
let wrapper;
const defaultProps = {
+ entity: 'project',
isLoading: false,
+ maxVariableLimit: mockVariables(projectString).length + 1,
variables: mockVariables(projectString),
};
+ const mockMaxVariableLimit = defaultProps.variables.length;
+
const createComponent = ({ props = {} } = {}) => {
wrapper = mountExtended(CiVariableTable, {
attachTo: document.body,
@@ -25,8 +31,15 @@ describe('Ci variable table', () => {
const findAddButton = () => wrapper.findByLabelText('Add');
const findEditButton = () => wrapper.findByLabelText('Edit');
const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
- const findHiddenValues = () => wrapper.findAll('[data-testid="hiddenValue"]');
- const findRevealedValues = () => wrapper.findAll('[data-testid="revealedValue"]');
+ const findHiddenValues = () => wrapper.findAllByTestId('hiddenValue');
+ const findLimitReachedAlerts = () => wrapper.findAllComponents(GlAlert);
+ const findRevealedValues = () => wrapper.findAllByTestId('revealedValue');
+ const findOptionsValues = (rowIndex) =>
+ wrapper.findAllByTestId('ci-variable-table-row-options').at(rowIndex).text();
+
+ const generateExceedsVariableLimitText = (entity, currentVariableCount, maxVariableLimit) => {
+ return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, { entity, currentVariableCount, maxVariableLimit });
+ };
beforeEach(() => {
createComponent();
@@ -66,6 +79,67 @@ describe('Ci variable table', () => {
it('displays the correct amount of variables', async () => {
expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
});
+
+ it('displays the correct variable options', async () => {
+ expect(findOptionsValues(0)).toBe('Protected, Expanded');
+ expect(findOptionsValues(1)).toBe('Masked');
+ });
+
+ it('enables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('When variables have exceeded the max limit', () => {
+ beforeEach(() => {
+ createComponent({ props: { maxVariableLimit: mockVariables(projectString).length } });
+ });
+
+ it('disables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('max limit reached alert', () => {
+ describe('when there is no variable limit', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { maxVariableLimit: 0 },
+ });
+ });
+
+ it('hides alert', () => {
+ expect(findLimitReachedAlerts().length).toBe(0);
+ });
+ });
+
+ describe('when variable limit exists', () => {
+ it('hides alert when limit has not been reached', () => {
+ createComponent();
+
+ expect(findLimitReachedAlerts().length).toBe(0);
+ });
+
+ it('shows alert when limit has been reached', () => {
+ const exceedsVariableLimitText = generateExceedsVariableLimitText(
+ defaultProps.entity,
+ defaultProps.variables.length,
+ mockMaxVariableLimit,
+ );
+
+ createComponent({
+ props: { maxVariableLimit: mockMaxVariableLimit },
+ });
+
+ expect(findLimitReachedAlerts().length).toBe(2);
+
+ expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+
+ expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ });
+ });
});
describe('Table click actions', () => {
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 03b77f80430..065e9fa6667 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -34,6 +34,7 @@ export const mockVariables = (kind) => {
key: 'my-var',
masked: false,
protected: true,
+ raw: false,
value: 'variable_value',
variableType: variableTypes.envType,
},
@@ -43,6 +44,7 @@ export const mockVariables = (kind) => {
key: 'secret',
masked: true,
protected: false,
+ raw: true,
value: 'another_value',
variableType: variableTypes.fileType,
},
@@ -63,6 +65,7 @@ const createDefaultVars = ({ withScope = true, kind } = {}) => {
return {
__typename: `Ci${kind}VariableConnection`,
+ limit: 200,
pageInfo: {
startCursor: 'adsjsd12kldpsa',
endCursor: 'adsjsd12kldpsa',
@@ -140,6 +143,7 @@ export const newVariable = {
export const createProjectProps = () => {
return {
componentName: 'ProjectVariable',
+ entity: 'project',
fullPath: '/namespace/project/',
id: 'gid://gitlab/Project/20',
mutationData: {
@@ -163,6 +167,7 @@ export const createProjectProps = () => {
export const createGroupProps = () => {
return {
componentName: 'GroupVariable',
+ entity: 'group',
fullPath: '/my-group',
id: 'gid://gitlab/Group/20',
mutationData: {
@@ -182,6 +187,7 @@ export const createGroupProps = () => {
export const createInstanceProps = () => {
return {
componentName: 'InstanceVariable',
+ entity: '',
mutationData: {
[ADD_MUTATION_ACTION]: addAdminVariable,
[UPDATE_MUTATION_ACTION]: updateAdminVariable,
@@ -195,3 +201,13 @@ export const createInstanceProps = () => {
},
};
};
+
+export const createGroupProvide = () => ({
+ isGroup: true,
+ isProject: false,
+});
+
+export const createProjectProvide = () => ({
+ isGroup: false,
+ isProject: true,
+});
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index 8d3130b45a6..a92a03fedb6 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -1,7 +1,13 @@
-import { GlAlert, GlFormInputGroup } from '@gitlab/ui';
+import { GlAlert, GlFormInputGroup, GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
import AgentToken from '~/clusters_list/components/agent_token.vue';
-import { I18N_AGENT_TOKEN, INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
+import {
+ I18N_AGENT_TOKEN,
+ INSTALL_AGENT_MODAL_ID,
+ NAME_MAX_LENGTH,
+ HELM_VERSION_POLICY_URL,
+} from '~/clusters_list/constants';
import { generateAgentRegistrationCommand } from '~/clusters_list/clusters_util';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -19,15 +25,17 @@ describe('InstallAgentModal', () => {
const findCodeBlock = () => wrapper.findComponent(CodeBlock);
const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findInput = () => wrapper.findComponent(GlFormInputGroup);
+ const findHelmVersionPolicyLink = () => wrapper.findComponent(GlLink);
+ const findHelmExternalLinkIcon = () => wrapper.findComponent(GlIcon);
- const createWrapper = () => {
+ const createWrapper = (newAgentName = agentName) => {
const provide = {
kasAddress,
kasVersion,
};
const propsData = {
- agentName,
+ agentName: newAgentName,
agentToken,
modalId,
};
@@ -35,6 +43,9 @@ describe('InstallAgentModal', () => {
wrapper = shallowMountExtended(AgentToken, {
provide,
propsData,
+ stubs: {
+ GlSprintf,
+ },
});
};
@@ -52,6 +63,17 @@ describe('InstallAgentModal', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallBody);
});
+ it('shows Helm version policy text with an external link', () => {
+ expect(wrapper.text()).toContain(
+ sprintf(I18N_AGENT_TOKEN.helmVersionText, { linkStart: '', linkEnd: ' ' }),
+ );
+ expect(findHelmVersionPolicyLink().attributes()).toMatchObject({
+ href: HELM_VERSION_POLICY_URL,
+ target: '_blank',
+ });
+ expect(findHelmExternalLinkIcon().props()).toMatchObject({ name: 'external-link', size: 12 });
+ });
+
it('shows advanced agent installation instructions', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.advancedInstallTitle);
});
@@ -79,9 +101,19 @@ describe('InstallAgentModal', () => {
it('shows code block with agent installation command', () => {
expect(findCodeBlock().props('code')).toContain(`helm upgrade --install ${agentName}`);
+ expect(findCodeBlock().props('code')).toContain(`--namespace gitlab-agent-${agentName}`);
expect(findCodeBlock().props('code')).toContain(`--set config.token=${agentToken}`);
expect(findCodeBlock().props('code')).toContain(`--set config.kasAddress=${kasAddress}`);
expect(findCodeBlock().props('code')).toContain(`--set image.tag=v${kasVersion}`);
});
+
+ it('truncates the namespace name if it exceeds the maximum length', () => {
+ const newAgentName = 'agent-name-that-is-too-long-and-needs-to-be-truncated-to-use';
+ createWrapper(newAgentName);
+
+ expect(findCodeBlock().props('code')).toContain(
+ `--namespace gitlab-agent-${newAgentName.substring(0, NAME_MAX_LENGTH)}`,
+ );
+ });
});
});
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index bff1e573dbd..2372ab30300 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlBanner } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
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';
@@ -12,10 +12,10 @@ import {
} from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+Vue.use(VueApollo);
describe('Agents', () => {
let wrapper;
@@ -34,9 +34,10 @@ describe('Agents', () => {
pageInfo = null,
trees = [],
count = 0,
+ queryResponse = null,
}) => {
const provide = provideData;
- const apolloQueryResponse = {
+ const queryResponseData = {
data: {
project: {
id: '1',
@@ -51,13 +52,12 @@ describe('Agents', () => {
},
},
};
+ const agentQueryResponse =
+ queryResponse || jest.fn().mockResolvedValue(queryResponseData, provide);
- const apolloProvider = createMockApollo([
- [getAgentsQuery, jest.fn().mockResolvedValue(apolloQueryResponse, provide)],
- ]);
+ const apolloProvider = createMockApollo([[getAgentsQuery, agentQueryResponse]]);
wrapper = shallowMount(Agents, {
- localVue,
apolloProvider,
propsData: {
...defaultProps,
@@ -313,24 +313,11 @@ describe('Agents', () => {
});
describe('when agents query is loading', () => {
- const mocks = {
- $apollo: {
- queries: {
- agents: {
- loading: true,
- },
- },
- },
- };
-
- beforeEach(async () => {
- wrapper = shallowMount(Agents, {
- mocks,
- propsData: defaultProps,
- provide: provideData,
+ beforeEach(() => {
+ createWrapper({
+ queryResponse: jest.fn().mockReturnValue(new Promise(() => {})),
});
-
- await nextTick();
+ return waitForPromises();
});
it('displays a loading icon', () => {
diff --git a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
index 197735d3c77..02b455d0b61 100644
--- a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
+++ b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
@@ -1,34 +1,31 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { ENTER_KEY } from '~/lib/utils/keys';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
describe('AvailableAgentsDropdown', () => {
let wrapper;
+ const configuredAgent = 'configured-agent';
+ const searchAgentName = 'search-agent';
+ const newAgentName = 'new-agent';
+
const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstAgentItem = () => findDropdownItems().at(0);
- const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
- const findCreateButton = () => wrapper.findByTestId('create-config-button');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findCreateButton = () => wrapper.findComponent(GlButton);
const createWrapper = ({ propsData }) => {
wrapper = shallowMountExtended(AvailableAgentsDropdown, {
propsData,
- stubs: { GlDropdown },
+ stubs: { GlCollapsibleListbox },
});
- wrapper.vm.$refs.dropdown.hide = jest.fn();
+ wrapper.vm.$refs.dropdown.closeAndFocus = jest.fn();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('there are agents available', () => {
const propsData = {
- availableAgents: ['configured-agent', 'search-agent', 'test-agent'],
+ availableAgents: [configuredAgent, searchAgentName, 'test-agent'],
isRegistering: false,
};
@@ -37,91 +34,93 @@ describe('AvailableAgentsDropdown', () => {
});
it('prompts to select an agent', () => {
- expect(findDropdown().props('text')).toBe(i18n.selectAgent);
+ expect(findDropdown().props('toggleText')).toBe(i18n.selectAgent);
});
describe('search agent', () => {
it('renders search button', () => {
- expect(findSearchInput().exists()).toBe(true);
+ expect(findDropdown().props('searchable')).toBe(true);
});
it('renders all agents when search term is empty', () => {
- expect(findDropdownItems()).toHaveLength(3);
+ expect(findDropdown().props('items')).toHaveLength(3);
});
it('renders only the agent searched for when the search item exists', async () => {
- await findSearchInput().vm.$emit('input', 'search-agent');
-
- expect(findDropdownItems()).toHaveLength(1);
- expect(findFirstAgentItem().text()).toBe('search-agent');
- });
+ findDropdown().vm.$emit('search', searchAgentName);
+ await nextTick();
- it('renders create button when search started', async () => {
- await findSearchInput().vm.$emit('input', 'new-agent');
-
- expect(findCreateButton().exists()).toBe(true);
+ expect(findDropdown().props('items')).toMatchObject([
+ { text: searchAgentName, value: searchAgentName },
+ ]);
});
- it("doesn't render create button when search item is found", async () => {
- await findSearchInput().vm.$emit('input', 'search-agent');
-
- expect(findCreateButton().exists()).toBe(false);
+ describe('create button', () => {
+ it.each`
+ condition | search | createButtonRendered
+ ${'is rendered'} | ${newAgentName} | ${true}
+ ${'is not rendered'} | ${''} | ${false}
+ ${'is not rendered'} | ${searchAgentName} | ${false}
+ `('$condition when search is "$search"', async ({ search, createButtonRendered }) => {
+ findDropdown().vm.$emit('search', search);
+ await nextTick();
+
+ expect(findCreateButton().exists()).toBe(createButtonRendered);
+ });
});
});
describe('select existing agent configuration', () => {
beforeEach(() => {
- findFirstAgentItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', configuredAgent);
});
- it('emits agentSelected with the name of the clicked agent', () => {
- expect(wrapper.emitted('agentSelected')).toEqual([['configured-agent']]);
+ it('emits `agentSelected` with the name of the clicked agent', () => {
+ expect(wrapper.emitted('agentSelected')).toEqual([[configuredAgent]]);
});
it('marks the clicked item as selected', () => {
- expect(findDropdown().props('text')).toBe('configured-agent');
- expect(findFirstAgentItem().props('isChecked')).toBe(true);
+ expect(findDropdown().props('toggleText')).toBe(configuredAgent);
});
});
describe('create new agent configuration', () => {
beforeEach(async () => {
- await findSearchInput().vm.$emit('input', 'new-agent');
+ findDropdown().vm.$emit('search', newAgentName);
+ await nextTick();
findCreateButton().vm.$emit('click');
});
it('emits agentSelected with the name of the clicked agent', () => {
- expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]);
+ expect(wrapper.emitted('agentSelected')).toEqual([[newAgentName]]);
});
it('marks the clicked item as selected', () => {
- expect(findDropdown().props('text')).toBe('new-agent');
+ expect(findDropdown().props('toggleText')).toBe(newAgentName);
});
});
describe('click enter to register new agent without configuration', () => {
beforeEach(async () => {
- await findSearchInput().vm.$emit('input', 'new-agent');
- await findSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ const dropdown = findDropdown();
+ dropdown.vm.$emit('search', newAgentName);
+ await nextTick();
+ await dropdown.trigger('keydown.enter');
});
it('emits agentSelected with the name of the clicked agent', () => {
- expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]);
+ expect(wrapper.emitted('agentSelected')).toEqual([[newAgentName]]);
});
it('marks the clicked item as selected', () => {
- expect(findDropdown().props('text')).toBe('new-agent');
- });
-
- it('closes the dropdown', () => {
- expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ expect(findDropdown().props('toggleText')).toBe(newAgentName);
});
});
});
describe('registration in progress', () => {
const propsData = {
- availableAgents: ['configured-agent'],
+ availableAgents: [configuredAgent],
isRegistering: true,
};
@@ -130,7 +129,7 @@ describe('AvailableAgentsDropdown', () => {
});
it('updates the text in the dropdown', () => {
- expect(findDropdown().props('text')).toBe(i18n.registeringAgent);
+ expect(findDropdown().props('toggleText')).toBe(i18n.registeringAgent);
});
it('displays a loading icon', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index a3f42c1f161..e8e705a6384 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -61,6 +61,10 @@ describe('Clusters', () => {
let captureException;
beforeEach(() => {
+ jest.spyOn(Sentry, 'withScope').mockImplementation((fn) => {
+ const mockScope = { setTag: () => {} };
+ fn(mockScope);
+ });
captureException = jest.spyOn(Sentry, 'captureException');
mock = new MockAdapter(axios);
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 09b1f80ff9b..1deebf8b75a 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -17,6 +17,10 @@ describe('Clusters store actions', () => {
describe('reportSentryError', () => {
beforeEach(() => {
+ jest.spyOn(Sentry, 'withScope').mockImplementation((fn) => {
+ const mockScope = { setTag: () => {} };
+ fn(mockScope);
+ });
captureException = jest.spyOn(Sentry, 'captureException');
});
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 6ad8a9de8d3..331a0a474a3 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
@@ -14,15 +14,15 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
</div>
</form>
</li>
- <li role=\\"presentation\\" class=\\"gl-new-dropdown-divider\\">
+ <li role=\\"presentation\\" class=\\"gl-dropdown-divider\\">
<hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
</li>
- <li role=\\"presentation\\" class=\\"gl-new-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
+ <li role=\\"presentation\\" class=\\"gl-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
<!---->
<!---->
<!---->
- <div class=\\"gl-new-dropdown-item-text-wrapper\\">
- <p class=\\"gl-new-dropdown-item-text-primary\\">
+ <div class=\\"gl-dropdown-item-text-wrapper\\">
+ <p class=\\"gl-dropdown-item-text-primary\\">
Upload file
</p>
<!---->
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index c1c2a125515..1a3cd36a8bb 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -10,7 +10,7 @@ import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/forma
import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
-import TopToolbar from '~/content_editor/components/top_toolbar.vue';
+import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
@@ -27,13 +27,14 @@ describe('ContentEditor', () => {
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
- const createWrapper = ({ markdown, autofocus } = {}) => {
+ const createWrapper = ({ markdown, autofocus, useBottomToolbar } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
markdown,
autofocus,
+ useBottomToolbar,
},
stubs: {
EditorStateObserver,
@@ -89,7 +90,19 @@ describe('ContentEditor', () => {
it('renders top toolbar component', () => {
createWrapper();
- expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(false);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(true);
+ });
+
+ it('renders bottom toolbar component', () => {
+ createWrapper({
+ useBottomToolbar: true,
+ });
+
+ expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(true);
+ expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(false);
});
describe('when setting initial content', () => {
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index 8f194ff32e2..c4bf21ba813 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -1,6 +1,6 @@
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TopToolbar from '~/content_editor/components/top_toolbar.vue';
+import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
@@ -11,7 +11,7 @@ describe('content_editor/components/top_toolbar', () => {
let trackingSpy;
const buildWrapper = () => {
- wrapper = shallowMountExtended(TopToolbar);
+ wrapper = shallowMountExtended(FormattingToolbar);
};
beforeEach(() => {
diff --git a/spec/frontend/content_editor/extensions/comment_spec.js b/spec/frontend/content_editor/extensions/comment_spec.js
new file mode 100644
index 00000000000..7d8ff28e4d7
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/comment_spec.js
@@ -0,0 +1,30 @@
+import Comment from '~/content_editor/extensions/comment';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/comment', () => {
+ let tiptapEditor;
+ let doc;
+ let comment;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Comment] });
+ ({
+ builders: { doc, comment },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ comment: { nodeType: Comment.name },
+ },
+ }));
+ });
+
+ describe('when typing the comment input rule', () => {
+ it('inserts a comment node', () => {
+ const expectedDoc = doc(comment());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: '<!-- ' });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index 5458a42532f..90d83820c70 100644
--- a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -1,5 +1,6 @@
import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import Bold from '~/content_editor/extensions/bold';
+import Comment from '~/content_editor/extensions/comment';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/gl_api_markdown_deserializer', () => {
@@ -7,19 +8,21 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
let doc;
let p;
let bold;
+ let comment;
let tiptapEditor;
beforeEach(() => {
tiptapEditor = createTestEditor({
- extensions: [Bold],
+ extensions: [Bold, Comment],
});
({
- builders: { doc, p, bold },
+ builders: { doc, p, bold, comment },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
+ comment: { nodeType: Comment.name },
},
}));
renderMarkdown = jest.fn();
@@ -33,7 +36,7 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
renderMarkdown.mockResolvedValueOnce(
- `<p><strong>${text}</strong></p><pre lang="javascript"></pre>`,
+ `<p><strong>${text}</strong></p><pre lang="javascript"></pre><!-- some comment -->`,
);
result = await deserializer.deserialize({
@@ -41,8 +44,9 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
schema: tiptapEditor.schema,
});
});
+
it('transforms HTML returned by render function to a ProseMirror document', async () => {
- const document = doc(p(bold(text)));
+ const document = doc(p(bold(text)), comment(' some comment '));
expect(result.document.toJSON()).toEqual(document.toJSON());
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 1bf23415052..2cd8b8a0d6f 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -3,6 +3,7 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Comment from '~/content_editor/extensions/comment';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
@@ -50,6 +51,7 @@ const {
bulletList,
code,
codeBlock,
+ comment,
details,
detailsContent,
div,
@@ -89,6 +91,7 @@ const {
bulletList: { nodeType: BulletList.name },
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
+ comment: { nodeType: Comment.name },
details: { nodeType: Details.name },
detailsContent: { nodeType: DetailsContent.name },
descriptionItem: { nodeType: DescriptionItem.name },
@@ -169,6 +172,17 @@ describe('markdownSerializer', () => {
);
});
+ it('correctly serializes a comment node', () => {
+ expect(serialize(paragraph('hi'), comment(' this is a\ncomment '))).toBe(
+ `
+hi
+
+<!-- this is a
+comment -->
+ `.trim(),
+ );
+ });
+
it('correctly serializes a line break', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
@@ -304,7 +318,7 @@ var y = 10;
expect(
serialize(
codeBlock(
- { language: 'json' },
+ { language: 'json', langParams: '' },
'this is not really json but just trying out whether this case works or not',
),
),
@@ -317,6 +331,23 @@ this is not really json but just trying out whether this case works or not
);
});
+ it('correctly serializes a code block with language parameters', () => {
+ expect(
+ serialize(
+ codeBlock(
+ { language: 'json', langParams: 'table' },
+ 'this is not really json:table but just trying out whether this case works or not',
+ ),
+ ),
+ ).toBe(
+ `
+\`\`\`json:table
+this is not really json:table but just trying out whether this case works or not
+\`\`\`
+ `.trim(),
+ );
+ });
+
it('correctly serializes emoji', () => {
expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:');
});
@@ -366,6 +397,26 @@ this is not really json but just trying out whether this case works or not
);
});
+ it.each`
+ width | height | outputAttributes
+ ${300} | ${undefined} | ${'width=300'}
+ ${undefined} | ${300} | ${'height=300'}
+ ${300} | ${300} | ${'width=300 height=300'}
+ ${'300%'} | ${'300px'} | ${'width="300%" height="300px"'}
+ `(
+ 'correctly serializes an image with width and height attributes',
+ ({ width, height, outputAttributes }) => {
+ const imageAttrs = { src: 'img.jpg', alt: 'foo bar' };
+
+ if (width) imageAttrs.width = width;
+ if (height) imageAttrs.height = height;
+
+ expect(serialize(paragraph(image(imageAttrs)))).toBe(
+ `![foo bar](img.jpg){${outputAttributes}}`,
+ );
+ },
+ );
+
it('does not serialize an image when src and canonicalSrc are empty', () => {
expect(serialize(paragraph(image({})))).toBe('');
});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 0768fa6e8df..0fa0e65cd26 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -11,10 +11,12 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Comment from '~/content_editor/extensions/comment';
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 Diagram from '~/content_editor/extensions/diagram';
import Emoji from '~/content_editor/extensions/emoji';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
@@ -211,10 +213,12 @@ export const createTiptapEditor = (extensions = []) =>
BulletList,
Code,
CodeBlockHighlight,
+ Comment,
DescriptionItem,
DescriptionList,
Details,
DetailsContent,
+ Diagram,
Emoji,
FootnoteDefinition,
FootnoteReference,
diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js
index e49b553e4b5..50b432943fb 100644
--- a/spec/frontend/crm/contact_form_wrapper_spec.js
+++ b/spec/frontend/crm/contact_form_wrapper_spec.js
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import ContactFormWrapper from '~/crm/contacts/components/contact_form_wrapper.vue';
-import ContactForm from '~/crm/components/form.vue';
+import CrmForm from '~/crm/components/crm_form.vue';
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
@@ -16,7 +16,7 @@ describe('Customer relations contact form wrapper', () => {
let wrapper;
let fakeApollo;
- const findContactForm = () => wrapper.findComponent(ContactForm);
+ const findCrmForm = () => wrapper.findComponent(CrmForm);
const $route = {
params: {
@@ -65,21 +65,21 @@ describe('Customer relations contact form wrapper', () => {
});
it('renders correct getQuery prop', () => {
- expect(findContactForm().props('getQueryNodePath')).toBe('group.contacts');
+ expect(findCrmForm().props('getQueryNodePath')).toBe('group.contacts');
});
it('renders correct mutation prop', () => {
- expect(findContactForm().props('mutation')).toBe(mutation);
+ expect(findCrmForm().props('mutation')).toBe(mutation);
});
it('renders correct additionalCreateParams prop', () => {
- expect(findContactForm().props('additionalCreateParams')).toMatchObject({
+ expect(findCrmForm().props('additionalCreateParams')).toMatchObject({
groupId: 'gid://gitlab/Group/26',
});
});
it('renders correct existingId prop', () => {
- expect(findContactForm().props('existingId')).toBe(existingId);
+ expect(findCrmForm().props('existingId')).toBe(existingId);
});
it('renders correct fields prop', () => {
@@ -101,15 +101,15 @@ describe('Customer relations contact form wrapper', () => {
{ name: 'description', label: 'Description' },
];
if (isEditMode) fields.push({ name: 'active', label: 'Active', required: true, bool: true });
- expect(findContactForm().props('fields')).toEqual(fields);
+ expect(findCrmForm().props('fields')).toEqual(fields);
});
it('renders correct title prop', () => {
- expect(findContactForm().props('title')).toBe(title);
+ expect(findCrmForm().props('title')).toBe(title);
});
it('renders correct successMessage prop', () => {
- expect(findContactForm().props('successMessage')).toBe(successMessage);
+ expect(findCrmForm().props('successMessage')).toBe(successMessage);
});
});
});
diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/crm_form_spec.js
index 57e28b396cf..eabcf5b1b1b 100644
--- a/spec/frontend/crm/form_spec.js
+++ b/spec/frontend/crm/crm_form_spec.js
@@ -5,7 +5,7 @@ import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import Form from '~/crm/components/form.vue';
+import CrmForm from '~/crm/components/crm_form.vue';
import routes from '~/crm/contacts/routes';
import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
@@ -81,7 +81,7 @@ describe('Reusable form component', () => {
const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
const mountComponent = (propsData) => {
- wrapper = shallowMountExtended(Form, {
+ wrapper = shallowMountExtended(CrmForm, {
router,
apolloProvider: fakeApollo,
propsData: { drawerOpen: true, ...propsData },
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index 9f26b9157e6..d795c585622 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -1,6 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue';
-import OrganizationForm from '~/crm/components/form.vue';
+import CrmForm from '~/crm/components/crm_form.vue';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql';
import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql';
@@ -8,7 +8,7 @@ import updateOrganizationMutation from '~/crm/organizations/components/graphql/u
describe('Customer relations organization form wrapper', () => {
let wrapper;
- const findOrganizationForm = () => wrapper.findComponent(OrganizationForm);
+ const findOrganizationForm = () => wrapper.findComponent(CrmForm);
const $apollo = {
queries: {
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index ce0c924bed2..9b96ce5d252 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -4,7 +4,7 @@ import Api from '~/api';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture } from '../helpers';
@@ -99,8 +99,8 @@ describe('deploy freeze store actions', () => {
});
describe('addFreezePeriod', () => {
- it('dispatch correct actions on adding a freeze period', () => {
- testAction(
+ it('dispatch correct actions on adding a freeze period', async () => {
+ await testAction(
actions.addFreezePeriod,
{},
state,
@@ -110,32 +110,33 @@ describe('deploy freeze store actions', () => {
{ type: 'receiveFreezePeriodSuccess' },
{ type: 'fetchFreezePeriods' },
],
- () =>
- expect(Api.createFreezePeriod).toHaveBeenCalledWith(state.projectId, {
- freeze_start: state.freezeStartCron,
- freeze_end: state.freezeEndCron,
- cron_timezone: state.selectedTimezoneIdentifier,
- }),
);
+
+ expect(Api.createFreezePeriod).toHaveBeenCalledWith(state.projectId, {
+ freeze_start: state.freezeStartCron,
+ freeze_end: state.freezeEndCron,
+ cron_timezone: state.selectedTimezoneIdentifier,
+ });
});
- it('should show flash error and set error in state on add failure', () => {
+ it('should show alert and set error in state on add failure', async () => {
Api.createFreezePeriod.mockRejectedValue();
- testAction(
+ await testAction(
actions.addFreezePeriod,
{},
state,
[],
[{ type: 'requestFreezePeriod' }, { type: 'receiveFreezePeriodError' }],
- () => expect(createFlash).toHaveBeenCalled(),
);
+
+ expect(createAlert).toHaveBeenCalled();
});
});
describe('updateFreezePeriod', () => {
- it('dispatch correct actions on updating a freeze period', () => {
- testAction(
+ it('dispatch correct actions on updating a freeze period', async () => {
+ await testAction(
actions.updateFreezePeriod,
{},
state,
@@ -145,33 +146,34 @@ describe('deploy freeze store actions', () => {
{ type: 'receiveFreezePeriodSuccess' },
{ type: 'fetchFreezePeriods' },
],
- () =>
- expect(Api.updateFreezePeriod).toHaveBeenCalledWith(state.projectId, {
- id: state.selectedId,
- freeze_start: state.freezeStartCron,
- freeze_end: state.freezeEndCron,
- cron_timezone: state.selectedTimezoneIdentifier,
- }),
);
+
+ expect(Api.updateFreezePeriod).toHaveBeenCalledWith(state.projectId, {
+ id: state.selectedId,
+ freeze_start: state.freezeStartCron,
+ freeze_end: state.freezeEndCron,
+ cron_timezone: state.selectedTimezoneIdentifier,
+ });
});
- it('should show flash error and set error in state on add failure', () => {
+ it('should show alert and set error in state on add failure', async () => {
Api.updateFreezePeriod.mockRejectedValue();
- testAction(
+ await testAction(
actions.updateFreezePeriod,
{},
state,
[],
[{ type: 'requestFreezePeriod' }, { type: 'receiveFreezePeriodError' }],
- () => expect(createFlash).toHaveBeenCalled(),
);
+
+ expect(createAlert).toHaveBeenCalled();
});
});
describe('fetchFreezePeriods', () => {
it('dispatch correct actions on fetchFreezePeriods', () => {
- testAction(
+ return testAction(
actions.fetchFreezePeriods,
{},
state,
@@ -183,26 +185,26 @@ describe('deploy freeze store actions', () => {
);
});
- it('should show flash error and set error in state on fetch variables failure', () => {
+ it('should show alert and set error in state on fetch variables failure', async () => {
Api.freezePeriods.mockRejectedValue();
- testAction(
+ await testAction(
actions.fetchFreezePeriods,
{},
state,
[{ type: types.REQUEST_FREEZE_PERIODS }],
[],
- () =>
- expect(createFlash).toHaveBeenCalledWith({
- message: 'There was an error fetching the deploy freezes.',
- }),
);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was an error fetching the deploy freezes.',
+ });
});
});
describe('deleteFreezePeriod', () => {
- it('dispatch correct actions on deleting a freeze period', () => {
- testAction(
+ it('dispatch correct actions on deleting a freeze period', async () => {
+ await testAction(
actions.deleteFreezePeriod,
freezePeriodFixture,
state,
@@ -211,20 +213,17 @@ describe('deploy freeze store actions', () => {
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS', payload: freezePeriodFixture.id },
],
[],
- () =>
- expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(
- state.projectId,
- freezePeriodFixture.id,
- ),
);
+
+ expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(state.projectId, freezePeriodFixture.id);
});
- it('should show flash error and set error in state on delete failure', () => {
+ it('should show alert and set error in state on delete failure', async () => {
jest.spyOn(logger, 'logError').mockImplementation();
const error = new Error();
Api.deleteFreezePeriod.mockRejectedValue(error);
- testAction(
+ await testAction(
actions.deleteFreezePeriod,
freezePeriodFixture,
state,
@@ -233,12 +232,11 @@ describe('deploy freeze store actions', () => {
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR', payload: freezePeriodFixture.id },
],
[],
- () => {
- expect(createFlash).toHaveBeenCalled();
-
- expect(logger.logError).toHaveBeenCalledWith('Unable to delete deploy freeze', error);
- },
);
+
+ expect(createAlert).toHaveBeenCalled();
+
+ expect(logger.logError).toHaveBeenCalledWith('Unable to delete deploy freeze', error);
});
});
});
diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
index 990f18d64c1..0bf69acd251 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -6,9 +6,21 @@ import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert, VARIANT_INFO } from '~/flash';
const createNewTokenPath = `${TEST_HOST}/create`;
const deployTokensHelpUrl = `${TEST_HOST}/help`;
+
+jest.mock('~/flash', () => {
+ const original = jest.requireActual('~/flash');
+
+ return {
+ __esModule: true,
+ ...original,
+ createAlert: jest.fn(),
+ };
+});
+
describe('New Deploy Token', () => {
let wrapper;
@@ -69,9 +81,69 @@ describe('New Deploy Token', () => {
expect(tokenUsername.props('value')).toBe('test token username');
expect(tokenValue.props('value')).toBe('test token');
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variant: VARIANT_INFO,
+ }),
+ );
});
}
+ it('should flash error message if token creation fails', async () => {
+ const mockAxios = new MockAdapter(axios);
+
+ const date = new Date();
+ const formInputs = wrapper.findAllComponents(GlFormInput);
+ const name = formInputs.at(0);
+ const username = formInputs.at(2);
+ name.vm.$emit('input', 'test name');
+ username.vm.$emit('input', 'test username');
+
+ const datepicker = wrapper.findAllComponents(GlDatepicker).at(0);
+ datepicker.vm.$emit('input', date);
+
+ const [
+ readRepo,
+ readRegistry,
+ writeRegistry,
+ readPackageRegistry,
+ writePackageRegistry,
+ ] = wrapper.findAllComponents(GlFormCheckbox).wrappers;
+ readRepo.vm.$emit('input', true);
+ readRegistry.vm.$emit('input', true);
+ writeRegistry.vm.$emit('input', true);
+ readPackageRegistry.vm.$emit('input', true);
+ writePackageRegistry.vm.$emit('input', true);
+
+ const expectedErrorMessage = 'Server error while creating a token';
+
+ mockAxios
+ .onPost(createNewTokenPath, {
+ deploy_token: {
+ name: 'test name',
+ expires_at: date.toISOString(),
+ username: 'test username',
+ read_repository: true,
+ read_registry: true,
+ write_registry: true,
+ read_package_registry: true,
+ write_package_registry: true,
+ },
+ })
+ .replyOnce(500, { message: expectedErrorMessage });
+
+ wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
+
+ await waitForPromises().then(() => nextTick());
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: expectedErrorMessage,
+ }),
+ );
+ });
+
it('should make a request to create a token on submit', () => {
const mockAxios = new MockAdapter(axios);
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index b3afcefe1ed..ac26873b692 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import mockDesign from '../mock_data/design';
const mockDesignWithPendingTodos = {
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 2b706d21f51..1acbf14db88 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -1,163 +1,229 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-dropdown-stub
+<gl-base-dropdown-stub
+ ariahaspopup="listbox"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
issueiid=""
projectpath=""
size="small"
- text="Showing latest version"
+ toggleid="dropdown-toggle-btn-2"
+ toggletext="Showing latest version"
variant="default"
>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischecked="true"
- ischeckitem="true"
- secondarytext=""
+ <!---->
+
+ <!---->
+
+ <ul
+ aria-labelledby="dropdown-toggle-btn-2"
+ class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
+ id="listbox"
+ role="listbox"
+ tabindex="-1"
>
- <strong>
- Version
- 2
- (latest)
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischeckitem="true"
- secondarytext=""
- >
- <strong>
- Version
- 1
-
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 2 (latest)
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
-</gl-dropdown-stub>
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 1
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ </ul>
+
+ <!---->
+
+</gl-base-dropdown-stub>
`;
exports[`Design management design version dropdown component renders design version list 1`] = `
-<gl-dropdown-stub
+<gl-base-dropdown-stub
+ ariahaspopup="listbox"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
issueiid=""
projectpath=""
size="small"
- text="Showing latest version"
+ toggleid="dropdown-toggle-btn-4"
+ toggletext="Showing latest version"
variant="default"
>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischecked="true"
- ischeckitem="true"
- secondarytext=""
+ <!---->
+
+ <!---->
+
+ <ul
+ aria-labelledby="dropdown-toggle-btn-4"
+ class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
+ id="listbox"
+ role="listbox"
+ tabindex="-1"
>
- <strong>
- Version
- 2
- (latest)
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckcentered="true"
- ischeckitem="true"
- secondarytext=""
- >
- <strong>
- Version
- 1
-
- </strong>
-
- <div
- class="gl-text-gray-600 gl-mt-1"
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 2 (latest)
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub
+ ischeckcentered="true"
>
- <div>
- Adminstrator
- </div>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </div>
- </gl-dropdown-item-stub>
-</gl-dropdown-stub>
+ <span
+ class="gl-display-flex gl-align-items-center"
+ >
+ <div
+ class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
+ >
+
+
+
+ </div>
+
+ <span
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Version 1
+ </span>
+
+ <span
+ class="gl-text-gray-600 gl-mt-1"
+ >
+ <span
+ class="gl-display-block"
+ >
+ Adminstrator
+ </span>
+
+ <time-ago-stub
+ class="text-1"
+ cssclass=""
+ time="2021-08-09T06:05:00Z"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </gl-listbox-item-stub>
+ </ul>
+
+ <!---->
+
+</gl-base-dropdown-stub>
`;
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index 7c26ab9739b..1e9f286a0ec 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlAvatar, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
@@ -32,7 +32,7 @@ describe('Design management design version dropdown component', () => {
mocks: {
$route,
},
- stubs: { GlSprintf },
+ stubs: { GlAvatar, GlCollapsibleListbox },
});
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
@@ -46,7 +46,9 @@ describe('Design management design version dropdown component', () => {
wrapper.destroy();
});
- const findVersionLink = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findVersionLink = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
it('renders design version dropdown button', async () => {
createComponent();
@@ -76,35 +78,36 @@ describe('Design management design version dropdown component', () => {
createComponent();
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
+
+ expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('displays latest version text when only 1 version is present', async () => {
createComponent({ maxVersions: 1 });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
+ expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('displays version text when the current version is not the latest', async () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(`Showing version #1`);
+ expect(findListbox().props('toggleText')).toBe(`Showing version #1`);
});
it('displays latest version text when the current version is the latest', async () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
+ expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('should have the same length as apollo query', async () => {
createComponent();
await nextTick();
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
+ expect(findAllListboxItems()).toHaveLength(wrapper.vm.allVersions.length);
});
it('should render TimeAgo', async () => {
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index b5dce4fc924..7bd9afab648 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -1,12 +1,14 @@
import { GlIcon } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/reports/codequality_report/constants';
+import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
import { multipleFindingsArr } from '../mock_data/diff_code_quality';
let wrapper;
const findIcon = () => wrapper.findComponent(GlIcon);
+const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`);
describe('DiffCodeQuality', () => {
afterEach(() => {
@@ -30,14 +32,17 @@ describe('DiffCodeQuality', () => {
expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1);
});
- it('renders correct amount of list items for codequality array and their description', async () => {
+ it('renders heading and correct amount of list items for codequality array and their description', async () => {
wrapper = createWrapper(multipleFindingsArr);
- const listItems = wrapper.findAll('li');
+ expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
- expect(wrapper.findAll('li').length).toBe(3);
+ const listItems = wrapper.findAll('li');
+ expect(wrapper.findAll('li').length).toBe(5);
listItems.wrappers.map((e, i) => {
- return expect(e.text()).toEqual(multipleFindingsArr[i].description);
+ return expect(e.text()).toContain(
+ `${multipleFindingsArr[i].severity} - ${multipleFindingsArr[i].description}`,
+ );
});
});
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
index 5ccd2002462..bf4a1a1c1f7 100644
--- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -1,10 +1,12 @@
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
-import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
+import { START_THREAD } from '~/diffs/i18n';
+
Vue.use(Vuex);
describe('DiffDiscussionReply', () => {
@@ -58,14 +60,42 @@ describe('DiffDiscussionReply', () => {
expect(wrapper.find('#test-form').exists()).toBe(true);
});
- it('should render a reply placeholder if there is no form', () => {
+ it('should render a reply placeholder button if there is no form', () => {
createComponent({
renderReplyPlaceholder: true,
hasForm: false,
});
- expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.findComponent(GlButton).text()).toBe(START_THREAD);
});
+
+ it.each`
+ userCanReply | hasForm | renderReplyPlaceholder | showButton
+ ${false} | ${false} | ${false} | ${false}
+ ${true} | ${false} | ${false} | ${false}
+ ${true} | ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true} | ${false}
+ ${true} | ${false} | ${true} | ${true}
+ ${false} | ${false} | ${true} | ${false}
+ `(
+ 'reply button existence is `$showButton` when userCanReply is `$userCanReply`, hasForm is `$hasForm` and renderReplyPlaceholder is `$renderReplyPlaceholder`',
+ ({ userCanReply, hasForm, renderReplyPlaceholder, showButton }) => {
+ getters = {
+ userCanReply: () => userCanReply,
+ };
+
+ store = new Vuex.Store({
+ getters,
+ });
+
+ createComponent({
+ renderReplyPlaceholder,
+ hasForm,
+ });
+
+ expect(wrapper.findComponent(GlButton).exists()).toBe(showButton);
+ },
+ );
});
it('renders a signed out widget when user is not logged in', () => {
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index e9a0e0745fd..5092ae6ab6e 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -5,9 +5,10 @@ import { createStore } from '~/mr_notes/stores';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('DiffDiscussions', () => {
let store;
let wrapper;
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index befab3b676b..7558592f6a4 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -1,25 +1,39 @@
export const multipleFindingsArr = [
{
severity: 'minor',
- description: 'Unexpected Debugger Statement.',
+ description: 'mocked minor Issue',
line: 2,
},
{
severity: 'major',
- description:
- 'Function `aVeryLongFunction` has 52 lines of code (exceeds 25 allowed). Consider refactoring.',
+ description: 'mocked major Issue',
line: 3,
},
{
- severity: 'minor',
- description: 'Arrow function has too many statements (52). Maximum allowed is 30.',
+ severity: 'info',
+ description: 'mocked info Issue',
+ line: 3,
+ },
+ {
+ severity: 'critical',
+ description: 'mocked critical Issue',
+ line: 3,
+ },
+ {
+ severity: 'blocker',
+ description: 'mocked blocker Issue',
line: 3,
},
];
-export const multipleFindings = {
+export const fiveFindings = {
+ filePath: 'index.js',
+ codequality: multipleFindingsArr.slice(0, 5),
+};
+
+export const threeFindings = {
filePath: 'index.js',
- codequality: multipleFindingsArr,
+ codequality: multipleFindingsArr.slice(0, 3),
};
export const singularFinding = {
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 87366cdbfc5..9e0ffbf757f 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -606,6 +606,50 @@ describe('DiffsStoreActions', () => {
params: { commit_id: '123', w: '0' },
});
});
+
+ describe('version parameters', () => {
+ const diffId = '4';
+ const startSha = 'abc';
+ const pathRoot = 'a/a/-/merge_requests/1';
+ let file;
+ let getters;
+
+ beforeAll(() => {
+ file = { load_collapsed_diff_url: '/load/collapsed/diff/url' };
+ getters = {};
+ });
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
+ });
+
+ it('fetches the data when there is no mergeRequestDiff', () => {
+ diffActions.loadCollapsedDiff({ commit() {}, getters, state }, file);
+
+ expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
+ params: expect.any(Object),
+ });
+ });
+
+ it.each`
+ desc | versionPath | start_sha | diff_id
+ ${'no additional version information'} | ${`${pathRoot}?search=terms`} | ${undefined} | ${undefined}
+ ${'the diff_id'} | ${`${pathRoot}?diff_id=${diffId}`} | ${undefined} | ${diffId}
+ ${'the start_sha'} | ${`${pathRoot}?start_sha=${startSha}`} | ${startSha} | ${undefined}
+ ${'all available version information'} | ${`${pathRoot}?diff_id=${diffId}&start_sha=${startSha}`} | ${startSha} | ${diffId}
+ `('fetches the data and includes $desc', ({ versionPath, start_sha, diff_id }) => {
+ jest.spyOn(axios, 'get').mockReturnValue(Promise.resolve({ data: {} }));
+
+ diffActions.loadCollapsedDiff(
+ { commit() {}, getters, state: { mergeRequestDiff: { version_path: versionPath } } },
+ file,
+ );
+
+ expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
+ params: expect.objectContaining({ start_sha, diff_id }),
+ });
+ });
+ });
});
describe('toggleFileDiscussions', () => {
diff --git a/spec/frontend/diffs/utils/merge_request_spec.js b/spec/frontend/diffs/utils/merge_request_spec.js
index 8c7b1e1f2a5..c070e8c004d 100644
--- a/spec/frontend/diffs/utils/merge_request_spec.js
+++ b/spec/frontend/diffs/utils/merge_request_spec.js
@@ -2,30 +2,64 @@ import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
import { diffMetadata } from '../mock_data/diff_metadata';
describe('Merge Request utilities', () => {
- const derivedMrInfo = {
+ const derivedBaseInfo = {
mrPath: '/gitlab-org/gitlab-test/-/merge_requests/4',
userOrGroup: 'gitlab-org',
project: 'gitlab-test',
id: '4',
};
+ const derivedVersionInfo = {
+ diffId: '4',
+ startSha: 'eb227b3e214624708c474bdab7bde7afc17cefcc',
+ };
+ const noVersion = {
+ diffId: undefined,
+ startSha: undefined,
+ };
const unparseableEndpoint = {
mrPath: undefined,
userOrGroup: undefined,
project: undefined,
id: undefined,
+ ...noVersion,
};
describe('getDerivedMergeRequestInformation', () => {
- const endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`;
+ let endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`;
it.each`
argument | response
- ${{ endpoint }} | ${derivedMrInfo}
+ ${{ endpoint }} | ${{ ...derivedBaseInfo, ...noVersion }}
${{}} | ${unparseableEndpoint}
${{ endpoint: undefined }} | ${unparseableEndpoint}
${{ endpoint: null }} | ${unparseableEndpoint}
`('generates the correct derived results based on $argument', ({ argument, response }) => {
expect(getDerivedMergeRequestInformation(argument)).toStrictEqual(response);
});
+
+ describe('version information', () => {
+ const bare = diffMetadata.latest_version_path;
+ endpoint = diffMetadata.merge_request_diffs[0].compare_path;
+
+ it('still gets the correct derived information', () => {
+ expect(getDerivedMergeRequestInformation({ endpoint })).toMatchObject(derivedBaseInfo);
+ });
+
+ it.each`
+ url | versionPart
+ ${endpoint} | ${derivedVersionInfo}
+ ${`${bare}?diff_id=${derivedVersionInfo.diffId}`} | ${{ ...derivedVersionInfo, startSha: undefined }}
+ ${`${bare}?start_sha=${derivedVersionInfo.startSha}`} | ${{ ...derivedVersionInfo, diffId: undefined }}
+ `(
+ 'generates the correct derived version information based on $url',
+ ({ url, versionPart }) => {
+ expect(getDerivedMergeRequestInformation({ endpoint: url })).toMatchObject(versionPart);
+ },
+ );
+
+ it('extracts nothing if there is no available version-like information in the URL', () => {
+ expect(getDerivedMergeRequestInformation({ endpoint: bare })).toMatchObject(noVersion);
+ });
+ });
});
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index 1475d451ab3..ff377494312 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -15,6 +15,9 @@ describe('Source Editor Toolbar button', () => {
propsData: {
...props,
},
+ stubs: {
+ GlButton,
+ },
});
};
@@ -52,9 +55,69 @@ describe('Source Editor Toolbar button', () => {
const btn = findButton();
expect(btn.props()).toMatchObject(customProps);
});
+
+ describe('CSS class', () => {
+ let blueprintClasses;
+
+ beforeEach(() => {
+ createComponent();
+ blueprintClasses = findButton().element.classList;
+ });
+
+ it.each`
+ cssClass | expectedExtraClasses
+ ${undefined} | ${['']}
+ ${''} | ${['']}
+ ${'foo'} | ${['foo']}
+ ${'foo bar'} | ${['foo', 'bar']}
+ `(
+ 'does set CSS class correctly when `class` is "$cssClass"',
+ ({ cssClass, expectedExtraClasses }) => {
+ createComponent({
+ button: {
+ ...defaultBtn,
+ class: cssClass,
+ },
+ });
+ const btn = findButton().element;
+ expectedExtraClasses.forEach((c) => {
+ if (c) {
+ expect(btn.classList.contains(c)).toBe(true);
+ } else {
+ expect(btn.classList).toEqual(blueprintClasses);
+ }
+ });
+ },
+ );
+ });
+ });
+
+ describe('data attributes', () => {
+ it.each`
+ description | data | expectedDataset
+ ${'does not set any attribute'} | ${undefined} | ${{}}
+ ${'does not set any attribute'} | ${[]} | ${{}}
+ ${'does not set any attribute'} | ${['foo']} | ${{}}
+ ${'does not set any attribute'} | ${'bar'} | ${{}}
+ ${'does set single attribute correctly'} | ${{ qaSelector: 'foo' }} | ${{ qaSelector: 'foo' }}
+ ${'does set multiple attributes correctly'} | ${{ qaSelector: 'foo', youCanSeeMe: true }} | ${{ qaSelector: 'foo', youCanSeeMe: 'true' }}
+ `('$description when data="$data"', ({ data, expectedDataset }) => {
+ createComponent({
+ button: {
+ data,
+ },
+ });
+ expect(findButton().element.dataset).toEqual(expect.objectContaining(expectedDataset));
+ });
});
describe('click handler', () => {
+ let clickEvent;
+
+ beforeEach(() => {
+ clickEvent = new Event('click');
+ });
+
it('fires the click handler on the button when available', async () => {
const spy = jest.fn();
createComponent({
@@ -63,20 +126,20 @@ describe('Source Editor Toolbar button', () => {
},
});
expect(spy).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
+ findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(spy).toHaveBeenCalled();
+ expect(spy).toHaveBeenCalledWith(clickEvent);
});
- it('emits the "click" event', async () => {
+ it('emits the "click" event, passing the event itself', async () => {
createComponent();
jest.spyOn(wrapper.vm, '$emit');
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
+ findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', clickEvent);
});
});
});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index 32126a5fd9a..c822a0bfeaf 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -30,6 +30,9 @@ import RulesYaml from './yaml_tests/positive_tests/rules.yml';
import ProjectPathYaml from './yaml_tests/positive_tests/project_path.yml';
import VariablesYaml from './yaml_tests/positive_tests/variables.yml';
import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml';
+import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml';
+import HooksYaml from './yaml_tests/positive_tests/hooks.yml';
+import SecretsYaml from './yaml_tests/positive_tests/secrets.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
@@ -43,8 +46,12 @@ import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_p
import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml';
import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
import TriggerNegative from './yaml_tests/negative_tests/trigger.yml';
+import VariablesInvalidOptionsYaml from './yaml_tests/negative_tests/variables/invalid_options.yml';
import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variables/invalid_syntax_desc.yml';
import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml';
+import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml';
+import HooksNegative from './yaml_tests/negative_tests/hooks.yml';
+import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml';
const ajv = new Ajv({
strictTypes: false,
@@ -77,9 +84,12 @@ describe('positive tests', () => {
FilterYaml,
IncludeYaml,
JobWhenYaml,
+ HooksYaml,
RulesYaml,
VariablesYaml,
ProjectPathYaml,
+ IdTokensYaml,
+ SecretsYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
@@ -103,9 +113,11 @@ describe('negative tests', () => {
// YAML
ArtifactsNegativeYaml,
CacheKeyNeative,
+ IdTokensNegativeYaml,
IncludeNegativeYaml,
JobWhenNegativeYaml,
RulesNegativeYaml,
+ VariablesInvalidOptionsYaml,
VariablesInvalidSyntaxDescYaml,
VariablesWrongSyntaxUsageExpand,
ProjectPathIncludeEmptyYaml,
@@ -113,7 +125,9 @@ describe('negative tests', () => {
ProjectPathIncludeLeadSlashYaml,
ProjectPathIncludeNoSlashYaml,
ProjectPathIncludeTailSlashYaml,
+ SecretsNegativeYaml,
TriggerNegative,
+ HooksNegative,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
index f5670376efc..29f4a0cd76d 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
@@ -16,3 +16,16 @@ cyclonedx not an array or string:
paths:
- foo
- bar
+
+# invalid artifacts:when
+artifacts-when-unknown:
+ artifacts:
+ when: unknown
+
+artifacts-when-array:
+ artifacts:
+ when: [always]
+
+artifacts-when-boolean:
+ artifacts:
+ when: true
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
index 3979c9ae2ac..9baed2a7922 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
@@ -51,12 +51,39 @@ cache-untracked-string:
cache:
untracked: 'true'
-when_integer:
+# invalid cache:when
+cache-when-integer:
script: echo "This job uses a cache."
cache:
when: 0
-when_not_reserved_keyword:
+cache-when-array:
+ script: echo "This job uses a cache."
+ cache:
+ when: [always]
+
+cache-when-boolean:
+ script: echo "This job uses a cache."
+ cache:
+ when: true
+
+cache-when-never:
script: echo "This job uses a cache."
cache:
when: 'never'
+
+# invalid cache:policy
+cache-policy-array:
+ script: echo "This job uses a cache."
+ cache:
+ policy: [push]
+
+cache-policy-boolean:
+ script: echo "This job uses a cache."
+ cache:
+ policy: true
+
+cache-when-unknown:
+ script: echo "This job uses a cache."
+ cache:
+ policy: unknown
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml
new file mode 100644
index 00000000000..e3366b0b6d3
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/hooks.yml
@@ -0,0 +1,10 @@
+job1:
+ hooks:
+ invalid_script:
+ - echo 'hello job1 invalid_script'
+ script: echo 'hello job1 script'
+
+job2:
+ hooks:
+ pre_get_sources_script: true
+ script: echo 'hello job1 script'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml
new file mode 100644
index 00000000000..aff2611f16c
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/id_tokens.yml
@@ -0,0 +1,11 @@
+id_token_with_wrong_aud_type:
+ id_tokens:
+ INVALID_ID_TOKEN:
+ aud:
+ invalid_prop: invalid
+
+id_token_with_extra_properties:
+ id_tokens:
+ INVALID_ID_TOKEN:
+ aud: 'https://gitlab.com'
+ sub: 'not a valid property'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml
new file mode 100644
index 00000000000..14ba930b394
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/secrets.yml
@@ -0,0 +1,39 @@
+job_with_secrets_without_vault:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ token: $TEST_TOKEN
+
+job_with_secrets_with_extra_properties:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault: test/db/password
+ extra_prop: TEST
+
+job_with_secrets_with_invalid_vault_property:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ invalid: TEST
+
+job_with_secrets_with_missing_required_vault_property:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ path: gitlab
+
+job_with_secrets_with_missing_required_engine_property:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ engine:
+ path: kv
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml
new file mode 100644
index 00000000000..aac4c4e456d
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_options.yml
@@ -0,0 +1,4 @@
+variables:
+ INVALID_OPTIONS:
+ value: "staging"
+ options: "staging" # must be an array
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
index 20c1fc2c50f..a5c9153ee13 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
@@ -23,3 +23,13 @@ cylonedx mixed list of string paths and globs:
cyclonedx:
- ./foo
- "bar/*.baz"
+
+# valid artifacts:when
+artifacts-when-on-failure:
+ artifacts:
+ when: on_failure
+
+artifacts-no-when:
+ artifacts:
+ paths:
+ - binaries/
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
index 75918cd2a1b..d50b74e1448 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
@@ -122,3 +122,20 @@ cache-untracked-false:
script: test
cache:
untracked: false
+
+# valid cache:policy
+cache-policy-push:
+ script: echo "This job uses a cache."
+ cache:
+ policy: push
+
+cache-policy-pull:
+ script: echo "This job uses a cache."
+ cache:
+ policy: pull
+
+cache-no-policy:
+ script: echo "This job uses a cache."
+ cache:
+ paths:
+ - binaries/
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml
new file mode 100644
index 00000000000..4d45c5528ea
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/hooks.yml
@@ -0,0 +1,10 @@
+default:
+ hooks:
+ pre_get_sources_script:
+ - echo 'hello default pre_get_sources_script'
+
+job1:
+ hooks:
+ pre_get_sources_script:
+ - echo 'hello job1 pre_get_sources_script'
+ script: echo 'hello job1 script'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml
new file mode 100644
index 00000000000..169b09ee56f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/id_tokens.yml
@@ -0,0 +1,11 @@
+valid_id_tokens:
+ script:
+ - echo $ID_TOKEN_1
+ - echo $ID_TOKEN_2
+ id_tokens:
+ ID_TOKEN_1:
+ aud: 'https://gitlab.com'
+ ID_TOKEN_2:
+ aud:
+ - 'https://aws.com'
+ - 'https://google.com'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml
new file mode 100644
index 00000000000..083cb4348ed
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/secrets.yml
@@ -0,0 +1,28 @@
+valid_job_with_secrets:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault: test/db/password
+
+valid_job_with_secrets_and_token:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault: test/db/password
+ token: $TEST_TOKEN
+
+valid_job_with_secrets_with_every_vault_keyword:
+ script:
+ - echo $TEST_DB_PASSWORD
+ secrets:
+ TEST_DB_PASSWORD:
+ vault:
+ engine:
+ name: test-engine
+ path: test
+ path: test/db
+ field: password
+ file: true
+ token: $TEST_TOKEN
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
index 53d020c432f..5c91de9be70 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
@@ -4,11 +4,18 @@ variables:
FOO:
value: "BAR"
description: "A single value variable"
- DEPLOY_ENVIRONMENT:
+ VAR_WITH_DESCRIPTION:
description: "A multi-value variable"
RAW_VAR:
value: "Hello $FOO"
expand: false
+ VAR_WITH_OPTIONS:
+ value: "staging"
+ options:
+ - "production"
+ - "staging"
+ - "canary"
+ description: "The deployment target. Set to 'production' by default."
rspec:
script: rspec
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 3e8c287df2f..33e4b4bfc8e 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -1,7 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import { Range, Position } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { EXTENSION_MARKDOWN_BUTTONS } from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
import axios from '~/lib/utils/axios_utils';
@@ -36,7 +38,7 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
- instance.use({ definition: EditorMarkdownExtension });
+ instance.use([{ definition: ToolbarExtension }, { definition: EditorMarkdownExtension }]);
});
afterEach(() => {
@@ -47,6 +49,16 @@ describe('Markdown Extension for Source Editor', () => {
resetHTMLFixture();
});
+ describe('toolbar', () => {
+ it('renders all the buttons', () => {
+ const btns = instance.toolbar.getAllItems();
+ expect(btns).toHaveLength(EXTENSION_MARKDOWN_BUTTONS.length);
+ EXTENSION_MARKDOWN_BUTTONS.forEach((btn, i) => {
+ expect(btns[i].id).toBe(btn.id);
+ });
+ });
+ });
+
describe('getSelectedText', () => {
it('does not fail if there is no selection and returns the empty string', () => {
jest.spyOn(instance, 'getSelection');
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 1c84350bd8e..82e3b50aeb8 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -1,7 +1,7 @@
/* eslint-disable import/no-commonjs, max-classes-per-file */
const path = require('path');
-const JSDOMEnvironment = require('jest-environment-jsdom');
+const { TestEnvironment } = require('jest-environment-jsdom');
const { ErrorWithStack } = require('jest-util');
const {
setGlobalDateToFakeDate,
@@ -11,10 +11,10 @@ const { TEST_HOST } = require('./__helpers__/test_constants');
const ROOT_PATH = path.resolve(__dirname, '../..');
-class CustomEnvironment extends JSDOMEnvironment {
- constructor(config, context) {
+class CustomEnvironment extends TestEnvironment {
+ constructor({ globalConfig, projectConfig }, context) {
// Setup testURL so that window.location is setup properly
- super({ ...config, testURL: TEST_HOST }, context);
+ super({ globalConfig, projectConfig: { ...projectConfig, testURL: TEST_HOST } }, context);
// Fake the `Date` for `jsdom` which fixes things like document.cookie
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
@@ -39,8 +39,7 @@ class CustomEnvironment extends JSDOMEnvironment {
},
});
- const { testEnvironmentOptions } = config;
- const { IS_EE } = testEnvironmentOptions;
+ const { IS_EE } = projectConfig.testEnvironmentOptions;
this.global.gon = {
ee: IS_EE,
};
diff --git a/spec/frontend/environments/environment_details_page_spec.js b/spec/frontend/environments/environment_details_page_spec.js
new file mode 100644
index 00000000000..5a02b34250f
--- /dev/null
+++ b/spec/frontend/environments/environment_details_page_spec.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from '../__helpers__/mock_apollo_helper';
+import waitForPromises from '../__helpers__/wait_for_promises';
+import EnvironmentsDetailPage from '../../../app/assets/javascripts/environments/environment_details/index.vue';
+import getEnvironmentDetails from '../../../app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql';
+
+describe('~/environments/environment_details/page.vue', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const createWrapper = () => {
+ const mockApollo = createMockApollo([
+ [getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedEnvironmentDetails)],
+ ]);
+
+ return mountExtended(EnvironmentsDetailPage, {
+ apolloProvider: mockApollo,
+ propsData: {
+ projectFullPath: resolvedEnvironmentDetails.data.project.fullPath,
+ environmentName: resolvedEnvironmentDetails.data.project.environment.name,
+ },
+ });
+ };
+
+ describe('when fetching data', () => {
+ it('should show a loading indicator', () => {
+ wrapper = createWrapper();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlTableLite).exists()).not.toBe(true);
+ });
+ });
+
+ describe('when data is fetched', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper();
+ await waitForPromises();
+ });
+
+ it('should render a table when query is loaded', async () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).not.toBe(true);
+ expect(wrapper.findComponent(GlTableLite).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
new file mode 100644
index 00000000000..401c10338c1
--- /dev/null
+++ b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
@@ -0,0 +1,127 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 1`] = `
+Object {
+ "commit": Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+ },
+ "created": "2022-10-17T07:44:17Z",
+ "deployed": "2022-10-17T07:44:43Z",
+ "id": "31",
+ "job": Object {
+ "label": "deploy-prod (#860)",
+ "webPath": "/gitlab-org/pipelinestest/-/jobs/860",
+ },
+ "status": "success",
+ "triggerer": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+}
+`;
+
+exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 2`] = `
+Object {
+ "commit": Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+ },
+ "created": "2022-10-17T07:44:17Z",
+ "deployed": "2022-10-17T07:44:43Z",
+ "id": "31",
+ "job": undefined,
+ "status": "success",
+ "triggerer": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+}
+`;
+
+exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 3`] = `
+Object {
+ "commit": Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+ },
+ "created": "2022-10-17T07:44:17Z",
+ "deployed": "",
+ "id": "31",
+ "job": null,
+ "status": "success",
+ "triggerer": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+}
+`;
+
+exports[`deployment_data_transformation_helper getAuthorFromCommit should be properly converted 1`] = `
+Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+}
+`;
+
+exports[`deployment_data_transformation_helper getAuthorFromCommit should be properly converted 2`] = `
+Object {
+ "avatar_url": "https://www.gravatar.com/avatar/91811aee1dec1b2655fa56f894e9e7c9?s=80&d=identicon",
+ "path": "mailto:azubov@gitlab.com",
+ "username": "Andrei Zubov",
+}
+`;
+
+exports[`deployment_data_transformation_helper getCommitFromDeploymentNode should get correclty formatted commit object 1`] = `
+Object {
+ "author": Object {
+ "avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
+ "path": "http://gdk.test:3000/root",
+ "username": "Administrator",
+ },
+ "commitRef": Object {
+ "name": "main",
+ },
+ "commitUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "shortSha": "0cb48dd5",
+ "tag": false,
+ "title": "Update .gitlab-ci.yml file",
+}
+`;
diff --git a/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
new file mode 100644
index 00000000000..8bb87c0a208
--- /dev/null
+++ b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
@@ -0,0 +1,96 @@
+import {
+ getAuthorFromCommit,
+ getCommitFromDeploymentNode,
+ convertToDeploymentTableRow,
+} from '~/environments/helpers/deployment_data_transformation_helper';
+
+describe('deployment_data_transformation_helper', () => {
+ const commitWithAuthor = {
+ id: 'gid://gitlab/CommitPresenter/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74',
+ shortId: '0cb48dd5',
+ message: 'Update .gitlab-ci.yml file',
+ webUrl:
+ 'http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74',
+ authorGravatar:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ authorName: 'Administrator',
+ authorEmail: 'admin@example.com',
+ author: {
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ webUrl: 'http://gdk.test:3000/root',
+ },
+ };
+
+ const commitWithourAuthor = {
+ id: 'gid://gitlab/CommitPresenter/02274a949a88c9aef68a29685d99bd9a661a7f9b',
+ shortId: '02274a94',
+ message: 'Commit message',
+ webUrl:
+ 'http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/02274a949a88c9aef68a29685d99bd9a661a7f9b',
+ authorGravatar:
+ 'https://www.gravatar.com/avatar/91811aee1dec1b2655fa56f894e9e7c9?s=80&d=identicon',
+ authorName: 'Andrei Zubov',
+ authorEmail: 'azubov@gitlab.com',
+ author: null,
+ };
+
+ const deploymentNode = {
+ id: 'gid://gitlab/Deployment/76',
+ iid: '31',
+ status: 'SUCCESS',
+ createdAt: '2022-10-17T07:44:17Z',
+ ref: 'main',
+ tag: false,
+ job: {
+ name: 'deploy-prod',
+ refName: 'main',
+ id: 'gid://gitlab/Ci::Build/860',
+ webPath: '/gitlab-org/pipelinestest/-/jobs/860',
+ },
+ commit: commitWithAuthor,
+ triggerer: {
+ id: 'gid://gitlab/User/1',
+ webUrl: 'http://gdk.test:3000/root',
+ name: 'Administrator',
+ avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
+ },
+ finishedAt: '2022-10-17T07:44:43Z',
+ };
+
+ const deploymentNodeWithNoJob = {
+ ...deploymentNode,
+ job: null,
+ finishedAt: null,
+ };
+
+ describe('getAuthorFromCommit', () => {
+ it.each([commitWithAuthor, commitWithourAuthor])('should be properly converted', (commit) => {
+ expect(getAuthorFromCommit(commit)).toMatchSnapshot();
+ });
+ });
+
+ describe('getCommitFromDeploymentNode', () => {
+ it('should throw an error when commit field is missing', () => {
+ const emptyDeploymentNode = {};
+
+ expect(() => getCommitFromDeploymentNode(emptyDeploymentNode)).toThrow();
+ });
+
+ it('should get correclty formatted commit object', () => {
+ expect(getCommitFromDeploymentNode(deploymentNode)).toMatchSnapshot();
+ });
+ });
+
+ describe('convertToDeploymentTableRow', () => {
+ const deploymentNodeWithEmptyJob = { ...deploymentNode, job: undefined };
+
+ it.each([deploymentNode, deploymentNodeWithEmptyJob, deploymentNodeWithNoJob])(
+ 'should be converted to proper table row data',
+ (node) => {
+ expect(convertToDeploymentTableRow(node)).toMatchSnapshot();
+ },
+ );
+ });
+});
diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
index 47f12f70056..f23bca54b55 100644
--- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
@@ -1,6 +1,6 @@
-import { GlToggle, GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlToggle } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import { mockTracking } from 'helpers/tracking_helper';
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
@@ -52,10 +52,10 @@ const getDefaultProps = () => ({
describe('Feature flag table', () => {
let wrapper;
let props;
- let badges;
+ let labels;
const createWrapper = (propsData, opts = {}) => {
- wrapper = shallowMount(FeatureFlagsTable, {
+ wrapper = mountExtended(FeatureFlagsTable, {
propsData,
provide: {
csrfToken: 'fakeToken',
@@ -70,18 +70,13 @@ describe('Feature flag table', () => {
provide: { csrfToken: 'fakeToken' },
});
- badges = wrapper.findAll('[data-testid="strategy-badge"]');
+ labels = wrapper.findAllByTestId('strategy-label');
});
beforeEach(() => {
props = getDefaultProps();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with an active scope and a standard rollout strategy', () => {
beforeEach(() => {
createWrapper(props);
@@ -101,7 +96,7 @@ describe('Feature flag table', () => {
});
it('Should render a status column', () => {
- const badge = wrapper.find('[data-testid="feature-flag-status-badge"]');
+ const badge = wrapper.findByTestId('feature-flag-status-badge');
expect(badge.exists()).toBe(true);
expect(trimText(badge.text())).toEqual('Active');
@@ -116,10 +111,10 @@ describe('Feature flag table', () => {
);
});
- it('should render an environments specs badge with active class', () => {
- const envColumn = wrapper.find('.js-feature-flag-environments');
+ it('should render an environments specs label', () => {
+ const strategyLabel = wrapper.findByTestId('strategy-label');
- expect(trimText(envColumn.findComponent(GlBadge).text())).toBe('All Users: All Environments');
+ expect(trimText(strategyLabel.text())).toBe('All Users: All Environments');
});
it('should render an actions column', () => {
@@ -167,29 +162,29 @@ describe('Feature flag table', () => {
});
it('shows All Environments if the environment scope is *', () => {
- expect(badges.at(0).text()).toContain('All Environments');
+ expect(labels.at(0).text()).toContain('All Environments');
});
it('shows the environment scope if another is set', () => {
- expect(badges.at(1).text()).toContain('production');
- expect(badges.at(1).text()).toContain('staging');
- expect(badges.at(2).text()).toContain('review/*');
+ expect(labels.at(1).text()).toContain('production');
+ expect(labels.at(1).text()).toContain('staging');
+ expect(labels.at(2).text()).toContain('review/*');
});
it('shows All Users for the default strategy', () => {
- expect(badges.at(0).text()).toContain('All Users');
+ expect(labels.at(0).text()).toContain('All Users');
});
it('shows the percent for a percent rollout', () => {
- expect(badges.at(1).text()).toContain('Percent of users - 50%');
+ expect(labels.at(1).text()).toContain('Percent of users - 50%');
});
it('shows the number of users for users with ID', () => {
- expect(badges.at(2).text()).toContain('User IDs - 4 users');
+ expect(labels.at(2).text()).toContain('User IDs - 4 users');
});
it('shows the name of a user list for user list', () => {
- expect(badges.at(3).text()).toContain('User List - test list');
+ expect(labels.at(3).text()).toContain('User List - test list');
});
it('renders a feature flag without an iid', () => {
diff --git a/spec/frontend/feature_flags/components/strategy_label_spec.js b/spec/frontend/feature_flags/components/strategy_label_spec.js
new file mode 100644
index 00000000000..c2d5ce10448
--- /dev/null
+++ b/spec/frontend/feature_flags/components/strategy_label_spec.js
@@ -0,0 +1,61 @@
+import { mount } from '@vue/test-utils';
+import StrategyLabel from '~/feature_flags/components/strategy_label.vue';
+
+const DEFAULT_PROPS = {
+ name: 'All Users',
+ parameters: 'parameters',
+ scopes: 'scope1, scope2',
+};
+
+describe('feature_flags/components/feature_flags_tab.vue', () => {
+ let wrapper;
+
+ const factory = (props = {}) =>
+ mount(
+ {
+ components: {
+ StrategyLabel,
+ },
+ render(h) {
+ return h(StrategyLabel, { props: this.$attrs, on: this.$listeners }, this.$slots.default);
+ },
+ },
+ {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ },
+ );
+
+ describe('render', () => {
+ let strategyLabel;
+
+ beforeEach(() => {
+ wrapper = factory({});
+ strategyLabel = wrapper.findComponent(StrategyLabel);
+ });
+
+ it('should show the strategy label with parameters and scope', () => {
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.name);
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.parameters);
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.scopes);
+ expect(strategyLabel.text()).toContain('All Users - parameters: scope1, scope2');
+ });
+ });
+
+ describe('without parameters', () => {
+ let strategyLabel;
+
+ beforeEach(() => {
+ wrapper = factory({ parameters: null });
+ strategyLabel = wrapper.findComponent(StrategyLabel);
+ });
+
+ it('should hide empty params and dash', () => {
+ expect(strategyLabel.text()).toContain(DEFAULT_PROPS.name);
+ expect(strategyLabel.text()).not.toContain(' - ');
+ expect(strategyLabel.text()).toContain('All Users: scope1, scope2');
+ });
+ });
+});
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index 22bac3fca15..d82081041d9 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -11,7 +11,7 @@ describe('feature highlight helper', () => {
let mockAxios;
const endpoint = '/-/callouts/dismiss';
const highlightId = '123';
- const { CREATED, INTERNAL_SERVER_ERROR } = httpStatusCodes;
+ const { INTERNAL_SERVER_ERROR } = httpStatusCodes;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -22,7 +22,7 @@ describe('feature highlight helper', () => {
});
it('calls persistent dismissal endpoint with highlightId', async () => {
- mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(CREATED);
+ mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(HTTP_STATUS_CREATED);
await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
});
diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
index 91457f10bf8..ebed477fa2f 100644
--- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
import eventHub from '~/filtered_search/event_hub';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import { TOKEN_TYPE_AUTHOR } from '~/vue_shared/components/filtered_search_bar/constants';
describe('Recent Searches Dropdown Content', () => {
let wrapper;
@@ -60,7 +61,7 @@ describe('Recent Searches Dropdown Content', () => {
items: [
'foo',
'author:@root label:~foo bar',
- [{ type: 'author_username', value: { data: 'toby', operator: '=' } }],
+ [{ type: TOKEN_TYPE_AUTHOR, value: { data: 'toby', operator: '=' } }],
],
isLocalStorageAvailable: true,
});
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 5e68725c03e..26af7af701b 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -8,7 +8,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
import { visitUrl, getParameterByName } from '~/lib/utils/url_utility';
@@ -130,14 +130,14 @@ describe('Filtered Search Manager', () => {
manager = new FilteredSearchManager({ page });
});
- it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
+ it('should not show an alert if an RecentSearchesServiceError is caught', () => {
jest
.spyOn(RecentSearchesService.prototype, 'fetch')
.mockImplementation(() => Promise.reject(new RecentSearchesServiceError()));
manager.setup();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
index 0e5c94edd05..28fcf0b7ec7 100644
--- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
@@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import waitForPromises from 'helpers/wait_for_promises';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
describe('Filtered Search Visual Tokens', () => {
let mock;
@@ -302,7 +303,7 @@ describe('Filtered Search Visual Tokens', () => {
});
const token = tokensContainer.querySelector('.js-visual-token');
- expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.classList.contains(FILTERED_SEARCH_TERM)).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.operator').innerText).toEqual('=');
expect(token.querySelector('.value')).toEqual(null);
@@ -430,7 +431,7 @@ describe('Filtered Search Visual Tokens', () => {
subject.addSearchVisualToken('search term');
const token = tokensContainer.querySelector('.js-visual-token');
- expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.classList.contains(FILTERED_SEARCH_TERM)).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.value')).toEqual(null);
});
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index e52ffa7bd9f..43c10090739 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -5,7 +5,7 @@ import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import VisualTokenValue from '~/filtered_search/visual_token_value';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
@@ -61,7 +61,7 @@ describe('Filtered Search Visual Tokens', () => {
};
await subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue);
- expect(createFlash.mock.calls.length).toBe(0);
+ expect(createAlert).toHaveBeenCalledTimes(0);
});
it('does nothing if user cannot be found', async () => {
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index fae1f4056fb..a71a41dc5c4 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -6,7 +6,6 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let_it_be(:admin) { create(:admin, name: 'root') }
let_it_be(:namespace) { create(:namespace, name: 'gitlab-test') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let_it_be(:early_mrs) do
@@ -14,21 +13,22 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
end
let_it_be(:mr) { create(:merge_request, source_project: project) }
+ let_it_be(:user) { project.owner }
it 'api/merge_requests/get.json' do
- get api("/projects/#{project.id}/merge_requests", admin)
+ get api("/projects/#{project.id}/merge_requests", user)
expect(response).to be_successful
end
it 'api/merge_requests/versions.json' do
- get api("/projects/#{project.id}/merge_requests/#{mr.iid}/versions", admin)
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/versions", user)
expect(response).to be_successful
end
it 'api/merge_requests/changes.json' do
- get api("/projects/#{project.id}/merge_requests/#{mr.iid}/changes", admin)
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/changes", user)
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index b14f402a7b9..d1dfd223419 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -6,25 +6,25 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let(:admin) { create(:admin, name: 'root') }
let(:namespace) { create(:namespace, name: 'gitlab-test') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
+ let(:user) { project.owner }
it 'api/projects/get.json' do
- get api("/projects/#{project.id}", admin)
+ get api("/projects/#{project.id}", user)
expect(response).to be_successful
end
it 'api/projects/get_empty.json' do
- get api("/projects/#{project_empty.id}", admin)
+ get api("/projects/#{project_empty.id}", user)
expect(response).to be_successful
end
it 'api/projects/branches/get.json' do
- get api("/projects/#{project.id}/repository/branches/#{project.default_branch}", admin)
+ get api("/projects/#{project.id}/repository/branches/#{project.default_branch}", user)
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/environments.rb b/spec/frontend/fixtures/environments.rb
new file mode 100644
index 00000000000..3ca5b50ac9c
--- /dev/null
+++ b/spec/frontend/fixtures/environments.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Environments (JavaScript fixtures)', feature_category: :environment_management do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin, username: 'administrator', email: 'admin@example.gitlab.com') }
+ let_it_be(:group) { create(:group, path: 'environments-group') }
+ let_it_be(:project) { create(:project, :repository, group: group, path: 'environments-project') }
+
+ let_it_be(:environment) { create(:environment, name: 'staging', project: project) }
+
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let_it_be(:deployment) do
+ create(:deployment, :success, environment: environment, deployable: nil)
+ end
+
+ let_it_be(:deployment_success) do
+ create(:deployment, :success, environment: environment, deployable: build)
+ end
+
+ let_it_be(:deployment_failed) do
+ create(:deployment, :failed, environment: environment, deployable: build)
+ end
+
+ let_it_be(:deployment_running) do
+ create(:deployment, :running, environment: environment, deployable: build)
+ end
+
+ describe GraphQL::Query, type: :request do
+ environment_details_query_path = 'environments/graphql/queries/environment_details.query.graphql'
+
+ it "graphql/#{environment_details_query_path}.json" do
+ query = get_graphql_query_as_string(environment_details_query_path)
+
+ post_graphql(query, current_user: admin,
+ variables:
+ {
+ projectFullPath: project.full_path,
+ environmentName: environment.name,
+ pageSize: 10
+ })
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
index 5aa466ef015..a1c7564d36e 100644
--- a/spec/frontend/fixtures/freeze_period.rb
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -13,15 +13,6 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
remove_repository(project)
end
- around do |example|
- freeze_time do
- # Mock time to sept 19 (intl. talk like a pirate day)
- travel_to(Time.utc(2020, 9, 19))
-
- example.run
- end
- end
-
describe API::FreezePeriods, '(JavaScript fixtures)', type: :request do
include ApiHelpers
diff --git a/spec/frontend/fixtures/releases.rb b/spec/frontend/fixtures/releases.rb
index fc344472588..c7e3d8fe804 100644
--- a/spec/frontend/fixtures/releases.rb
+++ b/spec/frontend/fixtures/releases.rb
@@ -6,9 +6,9 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
include ApiHelpers
include JavaScriptFixturesHelpers
- let_it_be(:admin) { create(:admin, username: 'administrator', email: 'admin@example.gitlab.com') }
let_it_be(:namespace) { create(:namespace, path: 'releases-namespace') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'releases-project') }
+ let_it_be(:user) { create(:user, email: 'user@example.gitlab.com', username: 'user1') }
let_it_be(:milestone_12_3) do
create(:milestone,
@@ -52,7 +52,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
project: project,
tag: 'v1.1',
name: 'The first release',
- author: admin,
+ author: user,
description: 'Best. Release. **Ever.** :rocket:',
created_at: Time.zone.parse('2018-12-3'),
released_at: Time.zone.parse('2018-12-10'))
@@ -105,19 +105,23 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
project: project,
tag: 'v1.2',
name: 'The second release',
- author: admin,
+ author: user,
description: 'An okay release :shrug:',
created_at: Time.zone.parse('2019-01-03'),
released_at: Time.zone.parse('2019-01-10'))
end
+ before do
+ project.add_owner(user)
+ end
+
after(:all) do
remove_repository(project)
end
describe API::Releases, type: :request do
it 'api/releases/release.json' do
- get api("/projects/#{project.id}/releases/#{release.tag}", admin)
+ get api("/projects/#{project.id}/releases/#{release.tag}", user)
expect(response).to be_successful
end
@@ -133,7 +137,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
it "graphql/#{all_releases_query_path}.json" do
query = get_graphql_query_as_string(all_releases_query_path)
- post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :releases)).to be_present
@@ -142,7 +146,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
it "graphql/#{one_release_query_path}.json" do
query = get_graphql_query_as_string(one_release_query_path)
- post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path, tagName: release.tag })
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :release)).to be_present
@@ -151,7 +155,7 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
it "graphql/#{one_release_for_editing_query_path}.json" do
query = get_graphql_query_as_string(one_release_for_editing_query_path)
- post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path, tagName: release.tag })
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :release)).to be_present
diff --git a/spec/frontend/fixtures/runner_instructions.rb b/spec/frontend/fixtures/runner_instructions.rb
new file mode 100644
index 00000000000..90a01c37479
--- /dev/null
+++ b/spec/frontend/fixtures/runner_instructions.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Runner Instructions (JavaScript fixtures)', feature_category: :runner do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ query_path = 'vue_shared/components/runner_instructions/graphql/queries'
+
+ describe GraphQL::Query do
+ describe 'get_runner_platforms.query.graphql', type: :request do
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}/get_runner_platforms.query.graphql")
+ end
+
+ it 'graphql/runner_instructions/get_runner_platforms.query.graphql.json' do
+ post_graphql(query)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ describe 'get_runner_setup.query.graphql', type: :request do
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}/get_runner_setup.query.graphql")
+ end
+
+ it 'graphql/runner_instructions/get_runner_setup.query.graphql.json' do
+ post_graphql(query, variables: { platform: 'linux', architecture: 'amd64' })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it 'graphql/runner_instructions/get_runner_setup.query.graphql.windows.json' do
+ post_graphql(query, variables: { platform: 'windows', architecture: 'amd64' })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/tabs.rb b/spec/frontend/fixtures/tabs.rb
index 697ff1c7c20..57ecb32e289 100644
--- a/spec/frontend/fixtures/tabs.rb
+++ b/spec/frontend/fixtures/tabs.rb
@@ -11,14 +11,14 @@ RSpec.describe 'GlTabsBehavior', '(JavaScript fixtures)', type: :helper do
it 'tabs/tabs.html' do
tabs = gl_tabs_nav({ data: { testid: 'tabs' } }) do
gl_tab_link_to('Foo', '#foo', item_active: true, data: { testid: 'foo-tab' }) +
- gl_tab_link_to('Bar', '#bar', item_active: false, data: { testid: 'bar-tab' }) +
- gl_tab_link_to('Qux', '#qux', item_active: false, data: { testid: 'qux-tab' })
+ gl_tab_link_to('Bar', '#bar', item_active: false, data: { testid: 'bar-tab' }) +
+ gl_tab_link_to('Qux', '#qux', item_active: false, data: { testid: 'qux-tab' })
end
panels = content_tag(:div, class: 'tab-content') do
content_tag(:div, 'Foo', { id: 'foo', class: 'tab-pane active', data: { testid: 'foo-panel' } }) +
- content_tag(:div, 'Bar', { id: 'bar', class: 'tab-pane', data: { testid: 'bar-panel' } }) +
- content_tag(:div, 'Qux', { id: 'qux', class: 'tab-pane', data: { testid: 'qux-panel' } })
+ content_tag(:div, 'Bar', { id: 'bar', class: 'tab-pane', data: { testid: 'bar-panel' } }) +
+ content_tag(:div, 'Qux', { id: 'qux', class: 'tab-pane', data: { testid: 'qux-panel' } })
end
@tabs = tabs + panels
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index a105b0b165c..ade36cd1637 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -12,6 +12,9 @@ import createFlash, {
jest.mock('@sentry/browser');
describe('Flash', () => {
+ const findTextContent = (containerSelector = '.flash-container') =>
+ document.querySelector(containerSelector).textContent.replace(/\s+/g, ' ').trim();
+
describe('hideFlash', () => {
let el;
@@ -99,7 +102,7 @@ describe('Flash', () => {
it('adds alert element into the document by default', () => {
alert = createAlert({ message: mockMessage });
- expect(document.querySelector('.flash-container').textContent.trim()).toBe(mockMessage);
+ expect(findTextContent()).toBe(mockMessage);
expect(document.querySelector('.flash-container .gl-alert')).not.toBeNull();
});
@@ -202,8 +205,7 @@ describe('Flash', () => {
message: mockMessage,
});
- const text = document.querySelector('.flash-container').textContent.trim();
- expect(text).toBe(`${mockTitle} ${mockMessage}`);
+ expect(findTextContent()).toBe(`${mockTitle} ${mockMessage}`);
});
});
@@ -319,6 +321,22 @@ describe('Flash', () => {
});
});
});
+
+ describe('when called multiple times', () => {
+ it('clears previous alerts', () => {
+ createAlert({ message: 'message 1' });
+ createAlert({ message: 'message 2' });
+
+ expect(findTextContent()).toBe('message 2');
+ });
+
+ it('preserves alerts when `preservePrevious` is true', () => {
+ createAlert({ message: 'message 1' });
+ createAlert({ message: 'message 2', preservePrevious: true });
+
+ expect(findTextContent()).toBe('message 1 message 2');
+ });
+ });
});
});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 68225f39c66..eeef92d4183 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -772,6 +772,7 @@ describe('GfmAutoComplete', () => {
input | output
${'~'} | ${unassignedLabels}
${'/label ~'} | ${unassignedLabels}
+ ${'/labels ~'} | ${unassignedLabels}
${'/relabel ~'} | ${unassignedLabels}
${'/unlabel ~'} | ${[]}
`('$input shows $output.length labels', expectLabels);
@@ -786,6 +787,7 @@ describe('GfmAutoComplete', () => {
input | output
${'~'} | ${allLabels}
${'/label ~'} | ${unassignedLabels}
+ ${'/labels ~'} | ${unassignedLabels}
${'/relabel ~'} | ${allLabels}
${'/unlabel ~'} | ${assignedLabels}
`('$input shows $output.length labels', expectLabels);
@@ -800,6 +802,7 @@ describe('GfmAutoComplete', () => {
input | output
${'~'} | ${assignedLabels}
${'/label ~'} | ${[]}
+ ${'/labels ~'} | ${[]}
${'/relabel ~'} | ${assignedLabels}
${'/unlabel ~'} | ${assignedLabels}
`('$input shows $output.length labels', expectLabels);
diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
new file mode 100644
index 00000000000..f1ed32a5f79
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
@@ -0,0 +1,202 @@
+import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { sprintf } from '~/locale';
+import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue';
+import * as utils from '~/gitlab_version_check/utils';
+import {
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ TRACKING_ACTIONS,
+ TRACKING_LABELS,
+} from '~/gitlab_version_check/constants';
+
+describe('SecurityPatchUpgradeAlertModal', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const defaultProps = {
+ currentVersion: '11.1.1',
+ };
+
+ const createComponent = (props = {}) => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+
+ wrapper = shallowMountExtended(SecurityPatchUpgradeAlertModal, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlModal,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ const expectDispatchedTracking = (action, label) => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
+ label,
+ property: defaultProps.currentVersion,
+ });
+ };
+
+ const findGlModal = () => wrapper.findComponent(GlModal);
+ const findGlModalTitle = () => wrapper.findByTestId('alert-modal-title');
+ const findGlModalBody = () => wrapper.findByTestId('alert-modal-body');
+ const findGlModalDetails = () => wrapper.findByTestId('alert-modal-details');
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findGlRemindButton = () => wrapper.findByTestId('alert-modal-remind-button');
+ const findGlUpgradeButton = () => wrapper.findByTestId('alert-modal-upgrade-button');
+
+ describe('template defaults', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders visible critical security alert modal', () => {
+ expect(findGlModal().props('visible')).toBe(true);
+ });
+
+ it('renders the modal title correctly', () => {
+ expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle);
+ });
+
+ it('renders modal body without suggested versions', () => {
+ expect(findGlModalBody().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, {
+ currentVersion: defaultProps.currentVersion,
+ }),
+ );
+ });
+
+ it('does not render modal details', () => {
+ expect(findGlModalDetails().exists()).toBe(false);
+ });
+
+ it(`tracks render ${TRACKING_LABELS.MODAL} correctly`, () => {
+ expectDispatchedTracking(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.DISMISS} when close button clicked`, async () => {
+ await findGlModal().vm.$emit('close');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS);
+ });
+
+ describe('Learn more link', () => {
+ it('renders with correct text and link', () => {
+ expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore);
+ expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.LEARN_MORE_LINK} when clicked`, async () => {
+ await findGlLink().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK);
+ });
+ });
+
+ describe('Remind me button', () => {
+ beforeEach(() => {
+ wrapper.vm.$refs.alertModal.hide = jest.fn();
+ });
+
+ it('renders with correct text', () => {
+ expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => {
+ await findGlRemindButton().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN);
+ });
+
+ it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => {
+ jest.spyOn(utils, 'setHideAlertModalCookie');
+ await findGlRemindButton().vm.$emit('click');
+
+ expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion);
+ });
+
+ it('hides the modal', async () => {
+ await findGlRemindButton().vm.$emit('click');
+
+ expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled();
+ });
+ });
+
+ describe('Upgrade button', () => {
+ it('renders with correct text and link', () => {
+ expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText);
+ expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it(`tracks click ${TRACKING_LABELS.UPGRADE_BTN_LINK} when clicked`, async () => {
+ await findGlUpgradeButton().vm.$emit('click');
+
+ expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK);
+ });
+
+ it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => {
+ jest.spyOn(utils, 'setHideAlertModalCookie');
+ await findGlUpgradeButton().vm.$emit('click');
+
+ expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion);
+ });
+ });
+ });
+
+ describe('template with latestStableVersions', () => {
+ const latestStableVersions = ['88.8.3', '89.9.9', '90.0.0'];
+
+ beforeEach(() => {
+ createComponent({ latestStableVersions });
+ });
+
+ it('renders modal body with suggested versions', () => {
+ expect(findGlModalBody().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, {
+ currentVersion: defaultProps.currentVersion,
+ latestStableVersions: latestStableVersions.join(', '),
+ }),
+ );
+ });
+ });
+
+ describe('template with details', () => {
+ const details = 'This is some details about the upgrade';
+
+ beforeEach(() => {
+ createComponent({ details });
+ });
+
+ it('renders modal details', () => {
+ expect(findGlModalDetails().text()).toBe(
+ sprintf(wrapper.vm.$options.i18n.modalDetails, { details }),
+ );
+ });
+ });
+
+ describe('when modal is hidden by cookie', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getHideAlertModalCookie').mockReturnValue(true);
+ createComponent();
+ });
+
+ it('renders modal with visibility false', () => {
+ expect(findGlModal().props('visible')).toBe(false);
+ });
+
+ it(`does not track render ${TRACKING_LABELS.MODAL} correctly`, () => {
+ expect(trackingSpy).not.toHaveBeenCalledWith(undefined, TRACKING_ACTIONS.RENDER, {
+ label: TRACKING_LABELS.MODAL,
+ property: defaultProps.currentVersion,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js
new file mode 100644
index 00000000000..665dacd5c47
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_spec.js
@@ -0,0 +1,84 @@
+import { GlAlert, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import SecurityPatchUpgradeAlert from '~/gitlab_version_check/components/security_patch_upgrade_alert.vue';
+import { UPGRADE_DOCS_URL, ABOUT_RELEASES_PAGE } from '~/gitlab_version_check/constants';
+
+describe('SecurityPatchUpgradeAlert', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const defaultProps = {
+ currentVersion: '99.9',
+ };
+
+ const createComponent = () => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+
+ wrapper = shallowMount(SecurityPatchUpgradeAlert, {
+ propsData: {
+ ...defaultProps,
+ },
+ stubs: {
+ GlAlert,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders non-dismissible GlAlert with version information', () => {
+ expect(findGlAlert().text()).toContain(
+ `You are currently on version ${defaultProps.currentVersion}.`,
+ );
+ expect(findGlAlert().props('dismissible')).toBe(false);
+ });
+
+ it('tracks render security_patch_upgrade_alert correctly', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
+ label: 'security_patch_upgrade_alert',
+ property: defaultProps.currentVersion,
+ });
+ });
+
+ it('renders GlLink with correct text and link', () => {
+ expect(findGlLink().text()).toBe('Learn more about this critical security release.');
+ expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
+ });
+
+ it('tracks click security_patch_upgrade_alert_learn_more when link is clicked', async () => {
+ await findGlLink().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'security_patch_upgrade_alert_learn_more',
+ property: defaultProps.currentVersion,
+ });
+ });
+
+ it('renders GlButton with correct text and link', () => {
+ expect(findGlButton().text()).toBe('Upgrade now');
+ expect(findGlButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it('tracks click security_patch_upgrade_alert_upgrade_now when button is clicked', async () => {
+ await findGlButton().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'security_patch_upgrade_alert_upgrade_now',
+ property: defaultProps.currentVersion,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gitlab_version_check/index_spec.js b/spec/frontend/gitlab_version_check/index_spec.js
index 8a11ff48bf2..92bc103cede 100644
--- a/spec/frontend/gitlab_version_check/index_spec.js
+++ b/spec/frontend/gitlab_version_check/index_spec.js
@@ -1,116 +1,52 @@
-import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
+import { createWrapper } from '@vue/test-utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initGitlabVersionCheck from '~/gitlab_version_check';
+import {
+ VERSION_CHECK_BADGE_NO_PROP_FIXTURE,
+ VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE,
+ VERSION_CHECK_BADGE_FIXTURE,
+ VERSION_CHECK_BADGE_FINDER,
+ VERSION_BADGE_TEXT,
+ SECURITY_PATCH_FIXTURE,
+ SECURITY_PATCH_FINDER,
+ SECURITY_PATCH_TEXT,
+ SECURITY_MODAL_FIXTURE,
+ SECURITY_MODAL_FINDER,
+ SECURITY_MODAL_TEXT,
+} from './mock_data';
describe('initGitlabVersionCheck', () => {
- let originalGon;
- let mock;
- let vueApps;
+ let wrapper;
- const defaultResponse = {
- code: 200,
- res: { severity: 'success' },
- };
-
- const dummyGon = {
- relative_url_root: '/',
- };
-
- const createApp = async (mockResponse, htmlClass) => {
- originalGon = window.gon;
-
- const response = {
- ...defaultResponse,
- ...mockResponse,
- };
-
- mock = new MockAdapter(axios);
- mock.onGet().replyOnce(response.code, response.res);
-
- setHTMLFixture(`<div class="${htmlClass}"></div>`);
-
- vueApps = await initGitlabVersionCheck();
+ const createApp = (fixture) => {
+ setHTMLFixture(fixture);
+ initGitlabVersionCheck();
+ wrapper = createWrapper(document.body);
};
afterEach(() => {
- mock.restore();
- window.gon = originalGon;
resetHTMLFixture();
});
- describe('with no .js-gitlab-version-check-badge elements', () => {
- beforeEach(async () => {
- await createApp();
- });
-
- it('does not make axios GET request', () => {
- expect(mock.history.get.length).toBe(0);
- });
-
- it('does not render the Version Check Badge', () => {
- expect(vueApps).toBeNull();
- });
- });
-
- describe('with .js-gitlab-version-check-badge element but API errors', () => {
- beforeEach(async () => {
- jest.spyOn(Sentry, 'captureException');
- await createApp({ code: 500, res: null }, 'js-gitlab-version-check-badge');
- });
-
- it('does make axios GET request', () => {
- expect(mock.history.get.length).toBe(1);
- expect(mock.history.get[0].url).toContain('/admin/version_check.json');
- });
-
- it('logs error to Sentry', () => {
- expect(Sentry.captureException).toHaveBeenCalled();
- });
-
- it('does not render the Version Check Badge', () => {
- expect(vueApps).toBeNull();
- });
- });
-
- describe('with .js-gitlab-version-check-badge element and successful API call', () => {
- beforeEach(async () => {
- await createApp({}, 'js-gitlab-version-check-badge');
- });
-
- it('does make axios GET request', () => {
- expect(mock.history.get.length).toBe(1);
- expect(mock.history.get[0].url).toContain('/admin/version_check.json');
- });
-
- it('does render the Version Check Badge', () => {
- expect(vueApps).toHaveLength(1);
- expect(vueApps[0]).toBeInstanceOf(Vue);
- });
- });
-
describe.each`
- root | description
- ${'/'} | ${'not used (uses its own (sub)domain)'}
- ${'/gitlab'} | ${'custom path'}
- ${'/service/gitlab'} | ${'custom path with 2 depth'}
- `('path for version_check.json', ({ root, description }) => {
- describe(`when relative url is ${description}: ${root}`, () => {
- beforeEach(async () => {
- originalGon = window.gon;
- window.gon = { ...dummyGon };
- window.gon.relative_url_root = root;
- await createApp({}, 'js-gitlab-version-check-badge');
- });
-
- it('reflects the relative url setting', () => {
- expect(mock.history.get.length).toBe(1);
-
- const pathRegex = new RegExp(`^${root}`);
- expect(mock.history.get[0].url).toMatch(pathRegex);
- });
+ description | fixture | finders | componentTexts
+ ${'with no version check elements'} | ${'<div></div>'} | ${[]} | ${[]}
+ ${'with version check badge el but no prop data'} | ${VERSION_CHECK_BADGE_NO_PROP_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
+ ${'with version check badge el but no severity data'} | ${VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]}
+ ${'with version check badge el and version data'} | ${VERSION_CHECK_BADGE_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[VERSION_BADGE_TEXT]}
+ ${'with security patch el'} | ${SECURITY_PATCH_FIXTURE} | ${[SECURITY_PATCH_FINDER]} | ${[SECURITY_PATCH_TEXT]}
+ ${'with security patch and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, VERSION_BADGE_TEXT]}
+ ${'with security modal el'} | ${SECURITY_MODAL_FIXTURE} | ${[SECURITY_MODAL_FINDER]} | ${[SECURITY_MODAL_TEXT]}
+ ${'with security modal, security patch, and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${SECURITY_MODAL_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, SECURITY_MODAL_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, SECURITY_MODAL_TEXT, VERSION_BADGE_TEXT]}
+ `('$description', ({ fixture, finders, componentTexts }) => {
+ beforeEach(() => {
+ createApp(fixture);
+ });
+
+ it(`correctly renders the Version Check Components`, () => {
+ const renderedComponentTexts = finders.map((f) => wrapper.find(f)?.element?.innerText.trim());
+
+ expect(renderedComponentTexts).toStrictEqual(componentTexts);
});
});
});
diff --git a/spec/frontend/gitlab_version_check/mock_data.js b/spec/frontend/gitlab_version_check/mock_data.js
new file mode 100644
index 00000000000..707d45550eb
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/mock_data.js
@@ -0,0 +1,22 @@
+export const VERSION_CHECK_BADGE_NO_PROP_FIXTURE =
+ '<div class="js-gitlab-version-check-badge"></div>';
+
+export const VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE = `<div class="js-gitlab-version-check-badge" data-version='{ "size": "sm" }'></div>`;
+
+export const VERSION_CHECK_BADGE_FIXTURE = `<div class="js-gitlab-version-check-badge" data-version='{ "severity": "success" }'></div>`;
+
+export const VERSION_CHECK_BADGE_FINDER = '[data-testid="badge-click-wrapper"]';
+
+export const VERSION_BADGE_TEXT = 'Up to date';
+
+export const SECURITY_PATCH_FIXTURE = `<div id="js-security-patch-upgrade-alert" data-current-version="15.1"></div>`;
+
+export const SECURITY_PATCH_FINDER = 'h2';
+
+export const SECURITY_PATCH_TEXT = 'Critical security upgrade available';
+
+export const SECURITY_MODAL_FIXTURE = `<div id="js-security-patch-upgrade-alert-modal" data-current-version="15.1" data-version='{ "details": "test details", "latest-stable-versions": "[]" }'></div>`;
+
+export const SECURITY_MODAL_FINDER = '[data-testid="alert-modal-title"]';
+
+export const SECURITY_MODAL_TEXT = 'Important notice - Critical security release';
diff --git a/spec/frontend/gitlab_version_check/utils_spec.js b/spec/frontend/gitlab_version_check/utils_spec.js
new file mode 100644
index 00000000000..6126d88dfec
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/utils_spec.js
@@ -0,0 +1,35 @@
+import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils';
+import { getHideAlertModalCookie, setHideAlertModalCookie } from '~/gitlab_version_check/utils';
+import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from '~/gitlab_version_check/constants';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ parseBoolean: jest.fn().mockReturnValue(true),
+ getCookie: jest.fn().mockReturnValue('true'),
+ setCookie: jest.fn(),
+}));
+
+describe('GitLab Version Check Utils', () => {
+ describe('setHideAlertModalCookie', () => {
+ it('properly generates a key based on the currentVersion and sets Cookie to `true`', () => {
+ const currentVersion = '99.9.9';
+
+ setHideAlertModalCookie(currentVersion);
+
+ expect(setCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`, true, {
+ expires: COOKIE_EXPIRATION,
+ });
+ });
+ });
+
+ describe('getHideAlertModalCookie', () => {
+ it('properly generates a key based on the currentVersion, fetches said Cooke, and parsesBoolean it', () => {
+ const currentVersion = '99.9.9';
+
+ const res = getHideAlertModalCookie(currentVersion);
+
+ expect(getCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`);
+ expect(parseBoolean).toHaveBeenCalledWith('true');
+ expect(res).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 091ec17d58e..140609161d4 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -1,7 +1,7 @@
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import appComponent from '~/groups/components/app.vue';
@@ -10,8 +10,6 @@ import groupItemComponent from '~/groups/components/group_item.vue';
import eventHub from '~/groups/event_hub';
import GroupsService from '~/groups/service/groups_service';
import GroupsStore from '~/groups/store/groups_store';
-import EmptyState from '~/groups/components/empty_state.vue';
-import GroupsComponent from '~/groups/components/groups.vue';
import axios from '~/lib/utils/axios_utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -43,7 +41,7 @@ describe('AppComponent', () => {
const createShallowComponent = ({ propsData = {} } = {}) => {
store.state.pageInfo = mockPageInfo;
- wrapper = shallowMount(appComponent, {
+ wrapper = shallowMountExtended(appComponent, {
propsData: {
store,
service,
@@ -51,6 +49,9 @@ describe('AppComponent', () => {
containerId: 'js-groups-tree',
...propsData,
},
+ scopedSlots: {
+ 'empty-state': '<div data-testid="empty-state" />',
+ },
mocks: {
$toast,
},
@@ -68,6 +69,7 @@ describe('AppComponent', () => {
mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
Vue.component('GroupFolder', groupFolderComponent);
Vue.component('GroupItem', groupItemComponent);
+ setWindowLocation('?filter=foobar');
document.body.innerHTML = `
<div id="js-groups-tree">
@@ -149,13 +151,13 @@ describe('AppComponent', () => {
expect(vm.fetchGroups).toHaveBeenCalledWith({
page: null,
- filterGroupsBy: null,
+ filterGroupsBy: 'foobar',
sortBy: null,
updatePagination: true,
archived: null,
});
return fetchPromise.then(() => {
- expect(vm.updateGroups).toHaveBeenCalled();
+ expect(vm.updateGroups).toHaveBeenCalledWith(mockSearchedGroups, true);
});
});
});
@@ -375,32 +377,16 @@ describe('AppComponent', () => {
expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups);
});
- it('should set `isSearchEmpty` prop based on groups count and `filter` query param', () => {
- setWindowLocation('?filter=foobar');
- createShallowComponent();
-
- vm.updateGroups(mockGroups);
-
- expect(vm.isSearchEmpty).toBe(false);
-
- vm.updateGroups([]);
-
- expect(vm.isSearchEmpty).toBe(true);
- });
-
describe.each`
- action | groups | fromSearch | shouldRenderEmptyState | searchEmpty
- ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${false}
- ${''} | ${[]} | ${false} | ${false} | ${false}
- ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${false} | ${false}
- ${'subgroups_and_projects'} | ${[]} | ${true} | ${false} | ${true}
+ groups | fromSearch | shouldRenderEmptyState | shouldRenderSearchEmptyState
+ ${[]} | ${false} | ${true} | ${false}
+ ${mockGroups} | ${false} | ${false} | ${false}
+ ${[]} | ${true} | ${false} | ${true}
`(
- 'when `action` is $action, `groups` is $groups, and `fromSearch` is $fromSearch',
- ({ action, groups, fromSearch, shouldRenderEmptyState, searchEmpty }) => {
+ 'when `groups` is $groups, and `fromSearch` is $fromSearch',
+ ({ groups, fromSearch, shouldRenderEmptyState, shouldRenderSearchEmptyState }) => {
it(`${shouldRenderEmptyState ? 'renders' : 'does not render'} empty state`, async () => {
- createShallowComponent({
- propsData: { action, renderEmptyState: true },
- });
+ createShallowComponent();
await waitForPromises();
@@ -408,28 +394,14 @@ describe('AppComponent', () => {
await nextTick();
- expect(wrapper.findComponent(EmptyState).exists()).toBe(shouldRenderEmptyState);
- expect(wrapper.findComponent(GroupsComponent).props('searchEmpty')).toBe(searchEmpty);
+ expect(wrapper.findByTestId('empty-state').exists()).toBe(shouldRenderEmptyState);
+ expect(wrapper.findByTestId('search-empty-state').exists()).toBe(
+ shouldRenderSearchEmptyState,
+ );
});
},
);
});
-
- describe('when `action` is subgroups_and_projects, `groups` is [], `fromSearch` is `false`, and `renderEmptyState` is `false`', () => {
- it('renders legacy empty state', async () => {
- createShallowComponent({
- propsData: { action: 'subgroups_and_projects' },
- });
-
- vm.updateGroups([], false);
-
- await nextTick();
-
- expect(
- document.querySelector('[data-testid="legacy-empty-state"]').classList.contains('hidden'),
- ).toBe(false);
- });
- });
});
describe('created', () => {
diff --git a/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
new file mode 100644
index 00000000000..be61ffa92b4
--- /dev/null
+++ b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { GlEmptyState } from '@gitlab/ui';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archived_projects_empty_state.vue';
+
+let wrapper;
+
+const defaultProvide = {
+ newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+};
+
+const createComponent = () => {
+ wrapper = mountExtended(ArchivedProjectsEmptyState, {
+ provide: defaultProvide,
+ });
+};
+
+describe('ArchivedProjectsEmptyState', () => {
+ it('renders empty state', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: ArchivedProjectsEmptyState.i18n.title,
+ svgPath: defaultProvide.newProjectIllustration,
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
new file mode 100644
index 00000000000..c4ace1be1f3
--- /dev/null
+++ b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { GlEmptyState } from '@gitlab/ui';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_projects_empty_state.vue';
+
+let wrapper;
+
+const defaultProvide = {
+ newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+};
+
+const createComponent = () => {
+ wrapper = mountExtended(SharedProjectsEmptyState, {
+ provide: defaultProvide,
+ });
+};
+
+describe('SharedProjectsEmptyState', () => {
+ it('renders empty state', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: SharedProjectsEmptyState.i18n.title,
+ svgPath: defaultProvide.newProjectIllustration,
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/empty_state_spec.js b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
index fbeaa32b1ec..75edc602fbf 100644
--- a/spec/frontend/groups/components/empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlEmptyState } from '@gitlab/ui';
import { mountExtended } from 'jest/__helpers__/vue_test_utils_helper';
-import EmptyState from '~/groups/components/empty_state.vue';
+import SubgroupsAndProjectsEmptyState from '~/groups/components/empty_states/subgroups_and_projects_empty_state.vue';
let wrapper;
@@ -16,7 +16,7 @@ const defaultProvide = {
};
const createComponent = ({ provide = {} } = {}) => {
- wrapper = mountExtended(EmptyState, {
+ wrapper = mountExtended(SubgroupsAndProjectsEmptyState, {
provide: {
...defaultProvide,
...provide,
@@ -30,18 +30,18 @@ afterEach(() => {
const findNewSubgroupLink = () =>
wrapper.findByRole('link', {
- name: new RegExp(EmptyState.i18n.withLinks.subgroup.title),
+ name: new RegExp(SubgroupsAndProjectsEmptyState.i18n.withLinks.subgroup.title),
});
const findNewProjectLink = () =>
wrapper.findByRole('link', {
- name: new RegExp(EmptyState.i18n.withLinks.project.title),
+ name: new RegExp(SubgroupsAndProjectsEmptyState.i18n.withLinks.project.title),
});
const findNewSubgroupIllustration = () =>
- wrapper.findByRole('img', { name: EmptyState.i18n.withLinks.subgroup.title });
+ wrapper.findByRole('img', { name: SubgroupsAndProjectsEmptyState.i18n.withLinks.subgroup.title });
const findNewProjectIllustration = () =>
- wrapper.findByRole('img', { name: EmptyState.i18n.withLinks.project.title });
+ wrapper.findByRole('img', { name: SubgroupsAndProjectsEmptyState.i18n.withLinks.project.title });
-describe('EmptyState', () => {
+describe('SubgroupsAndProjectsEmptyState', () => {
describe('when user has permission to create a subgroup', () => {
it('renders `Create new subgroup` link', () => {
createComponent();
@@ -69,8 +69,8 @@ describe('EmptyState', () => {
createComponent({ provide: { canCreateSubgroups: false, canCreateProjects: false } });
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
- title: EmptyState.i18n.withoutLinks.title,
- description: EmptyState.i18n.withoutLinks.description,
+ title: SubgroupsAndProjectsEmptyState.i18n.withoutLinks.title,
+ description: SubgroupsAndProjectsEmptyState.i18n.withoutLinks.description,
svgPath: defaultProvide.emptySubgroupIllustration,
});
});
diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js
index 823d2ed286a..9965b608f27 100644
--- a/spec/frontend/groups/components/group_name_and_path_spec.js
+++ b/spec/frontend/groups/components/group_name_and_path_spec.js
@@ -398,7 +398,7 @@ describe('GroupNameAndPath', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().findByRole('link', { name: 'Learn more' }).attributes('href')).toBe(
- helpPagePath('user/group/index', {
+ helpPagePath('user/group/manage', {
anchor: 'change-a-groups-path',
}),
);
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 0cbb6cc8309..cae29a8f15a 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -16,7 +16,6 @@ describe('GroupsComponent', () => {
const defaultPropsData = {
groups: mockGroups,
pageInfo: mockPageInfo,
- searchEmpty: false,
};
const createComponent = ({ propsData } = {}) => {
@@ -69,14 +68,5 @@ describe('GroupsComponent', () => {
expect(findPaginationLinks().exists()).toBe(true);
expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
});
-
- it('should render empty search message when `searchEmpty` is `true`', () => {
- createComponent({ propsData: { searchEmpty: true } });
-
- expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
- title: GroupsComponent.i18n.emptyStateTitle,
- description: GroupsComponent.i18n.emptyStateDescription,
- });
- });
});
});
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index b615679dcc5..d1ae2c4be17 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -1,11 +1,13 @@
import { GlSorting, GlSortingItem, GlTab } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { createLocalVue } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import OverviewTabs from '~/groups/components/overview_tabs.vue';
import GroupsApp from '~/groups/components/app.vue';
import GroupFolderComponent from '~/groups/components/group_folder.vue';
+import SubgroupsAndProjectsEmptyState from '~/groups/components/empty_states/subgroups_and_projects_empty_state.vue';
+import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_projects_empty_state.vue';
+import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archived_projects_empty_state.vue';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
import { createRouter } from '~/groups/init_overview_tabs';
@@ -17,9 +19,9 @@ import {
OVERVIEW_TABS_SORTING_ITEMS,
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
-const localVue = createLocalVue();
-localVue.component('GroupFolder', GroupFolderComponent);
+Vue.component('GroupFolder', GroupFolderComponent);
const router = createRouter();
const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
@@ -57,7 +59,6 @@ describe('OverviewTabs', () => {
...defaultProvide,
...provide,
},
- localVue,
mocks: { $route: route, $router: routerMock },
});
@@ -71,6 +72,7 @@ describe('OverviewTabs', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet({ data: [] });
});
afterEach(() => {
@@ -78,7 +80,7 @@ describe('OverviewTabs', () => {
axiosMock.restore();
});
- it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => {
+ it('renders `Subgroups and projects` tab with `GroupsApp` component with correct empty state', async () => {
await createComponent();
const tabPanel = findTabPanels().at(0);
@@ -92,11 +94,14 @@ describe('OverviewTabs', () => {
store: new GroupsStore({ showSchemaMarkup: true }),
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
hideProjects: false,
- renderEmptyState: true,
});
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(SubgroupsAndProjectsEmptyState).exists()).toBe(true);
});
- it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => {
+ it('renders `Shared projects` tab and renders `GroupsApp` component with correct empty state after clicking tab', async () => {
await createComponent();
const tabPanel = findTabPanels().at(1);
@@ -113,13 +118,16 @@ describe('OverviewTabs', () => {
store: new GroupsStore(),
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]),
hideProjects: false,
- renderEmptyState: false,
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(SharedProjectsEmptyState).exists()).toBe(true);
});
- it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => {
+ it('renders `Archived projects` tab and renders `GroupsApp` component with correct empty state after clicking tab', async () => {
await createComponent();
const tabPanel = findTabPanels().at(2);
@@ -136,10 +144,13 @@ describe('OverviewTabs', () => {
store: new GroupsStore(),
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]),
hideProjects: false,
- renderEmptyState: false,
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(ArchivedProjectsEmptyState).exists()).toBe(true);
});
it('sets `lazy` prop to `false` for initially active tab and `true` for all other tabs', async () => {
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index b0bfe2b45f0..c714c269ca0 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -180,7 +180,6 @@ describe('HeaderSearchApp', () => {
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(false);
- // only one event emmited from findHeaderSearchInput().vm.$emit('click');
expect(wrapper.emitted().expandSearchBar.length).toBe(1);
});
});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index b7349b8fed1..294f5eee863 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -3,16 +3,12 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import RightPane from '~/ide/components/panes/right.vue';
-import SwitchEditorsView from '~/ide/components/switch_editors/switch_editors_view.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
import extendStore from '~/ide/stores/extend';
-import { __ } from '~/locale';
Vue.use(Vuex);
-const SWITCH_EDITORS_VIEW_NAME = 'switch-editors';
-
describe('ide/components/panes/right.vue', () => {
let wrapper;
let store;
@@ -45,7 +41,6 @@ describe('ide/components/panes/right.vue', () => {
it('renders collapsible-sidebar', () => {
expect(wrapper.findComponent(CollapsibleSidebar).props()).toMatchObject({
side: 'right',
- initOpenView: SWITCH_EDITORS_VIEW_NAME,
});
});
});
@@ -130,32 +125,4 @@ describe('ide/components/panes/right.vue', () => {
);
});
});
-
- describe('switch editors tab', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it.each`
- desc | canUseNewWebIde | expectedShow
- ${'is shown'} | ${true} | ${true}
- ${'is not shown'} | ${false} | ${false}
- `('with canUseNewWebIde=$canUseNewWebIde, $desc', async ({ canUseNewWebIde, expectedShow }) => {
- Object.assign(store.state, { canUseNewWebIde });
-
- await nextTick();
-
- expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- show: expectedShow,
- title: __('Switch editors'),
- views: [
- { component: SwitchEditorsView, name: SWITCH_EDITORS_VIEW_NAME, keepAlive: true },
- ],
- }),
- ]),
- );
- });
- });
});
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index 545924c9c11..d82b97561f0 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -185,7 +185,7 @@ describe('IDE pipelines list', () => {
},
);
- expect(wrapper.text()).toContain('Found errors in your .gitlab-ci.yml:');
+ expect(wrapper.text()).toContain('Unable to create pipeline');
expect(wrapper.text()).toContain(yamlError);
});
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 9921d8cba18..211fee31a9c 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -4,13 +4,17 @@ import { editor as monacoEditor, Range } from 'monaco-editor';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
-import '~/behaviors/markdown/render_gfm';
import waitForPromises from 'helpers/wait_for_promises';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
-import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants';
+import {
+ EDITOR_CODE_INSTANCE_FN,
+ EDITOR_DIFF_INSTANCE_FN,
+ EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
+} from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
+import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/editor/source_editor';
import RepoEditor from '~/ide/components/repo_editor.vue';
import { leftSidebarViews, FILE_VIEW_MODE_PREVIEW, viewerTypes } from '~/ide/constants';
@@ -22,6 +26,9 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
import SourceEditorInstance from '~/editor/source_editor_instance';
import { file } from '../helpers';
+jest.mock('~/behaviors/markdown/render_gfm');
+jest.mock('~/editor/extensions/source_editor_ci_schema_ext');
+
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const CURRENT_PROJECT_ID = 'gitlab-org/gitlab';
@@ -46,6 +53,12 @@ const dummyFile = {
tempFile: true,
active: true,
},
+ ciConfig: {
+ ...file(EXTENSION_CI_SCHEMA_FILE_NAME_MATCH),
+ content: '',
+ tempFile: true,
+ active: true,
+ },
empty: {
...file('empty'),
tempFile: false,
@@ -101,6 +114,7 @@ describe('RepoEditor', () => {
let createDiffInstanceSpy;
let createModelSpy;
let applyExtensionSpy;
+ let removeExtensionSpy;
let extensionsStore;
const waitForEditorSetup = () =>
@@ -108,7 +122,7 @@ describe('RepoEditor', () => {
vm.$once('editorSetup', resolve);
});
- const createComponent = async ({ state = {}, activeFile = dummyFile.text } = {}) => {
+ const createComponent = async ({ state = {}, activeFile = dummyFile.text, flags = {} } = {}) => {
const store = prepareStore(state, activeFile);
wrapper = shallowMount(RepoEditor, {
store,
@@ -118,6 +132,9 @@ describe('RepoEditor', () => {
mocks: {
ContentViewer,
},
+ provide: {
+ glFeatures: flags,
+ },
});
await waitForPromises();
vm = wrapper.vm;
@@ -137,6 +154,7 @@ describe('RepoEditor', () => {
createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN);
createModelSpy = jest.spyOn(monacoEditor, 'createModel');
applyExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'use');
+ removeExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'unuse');
jest.spyOn(service, 'getFileData').mockResolvedValue();
jest.spyOn(service, 'getRawFileData').mockResolvedValue();
});
@@ -177,6 +195,76 @@ describe('RepoEditor', () => {
});
});
+ describe('schema registration for .gitlab-ci.yml', () => {
+ const setup = async (activeFile, flagIsOn = true) => {
+ await createComponent({
+ flags: {
+ schemaLinting: flagIsOn,
+ },
+ });
+ vm.editor.registerCiSchema = jest.fn();
+ if (activeFile) {
+ wrapper.setProps({ file: activeFile });
+ }
+ await waitForPromises();
+ await nextTick();
+ };
+ it.each`
+ flagIsOn | activeFile | shouldUseExtension | desc
+ ${false} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`}
+ ${true} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`}
+ ${false} | ${dummyFile.ciConfig} | ${false} | ${`file is CI config; should NOT`}
+ ${true} | ${dummyFile.ciConfig} | ${true} | ${`file is CI config; should`}
+ `(
+ 'when the flag is "$flagIsOn", $desc use extension',
+ async ({ flagIsOn, activeFile, shouldUseExtension }) => {
+ await setup(activeFile, flagIsOn);
+
+ if (shouldUseExtension) {
+ expect(applyExtensionSpy).toHaveBeenCalledWith({
+ definition: CiSchemaExtension,
+ });
+ } else {
+ expect(applyExtensionSpy).not.toHaveBeenCalledWith({
+ definition: CiSchemaExtension,
+ });
+ }
+ },
+ );
+ it('stores the fetched extension and does not double-fetch the schema', async () => {
+ await setup();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(0);
+
+ wrapper.setProps({ file: dummyFile.ciConfig });
+ await waitForPromises();
+ await nextTick();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(1);
+ expect(vm.CiSchemaExtension).toEqual(CiSchemaExtension);
+ expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1);
+
+ wrapper.setProps({ file: dummyFile.markdown });
+ await waitForPromises();
+ await nextTick();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(1);
+ expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1);
+
+ wrapper.setProps({ file: dummyFile.ciConfig });
+ await waitForPromises();
+ await nextTick();
+ expect(CiSchemaExtension).toHaveBeenCalledTimes(1);
+ expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(2);
+ });
+ it('unuses the existing CI extension if the new model is not CI config', async () => {
+ await setup(dummyFile.ciConfig);
+
+ expect(removeExtensionSpy).not.toHaveBeenCalled();
+ wrapper.setProps({ file: dummyFile.markdown });
+ await waitForPromises();
+ await nextTick();
+ expect(removeExtensionSpy).toHaveBeenCalledWith(CiSchemaExtension);
+ });
+ });
+
describe('when file is markdown', () => {
let mock;
let activeFile;
diff --git a/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js b/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
deleted file mode 100644
index 7a958391fea..00000000000
--- a/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { logError } from '~/lib/logger';
-import { __ } from '~/locale';
-import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import SwitchEditorsView, {
- MSG_ERROR_ALERT,
- MSG_CONFIRM,
- MSG_TITLE,
- MSG_LEARN_MORE,
- MSG_DESCRIPTION,
-} from '~/ide/components/switch_editors/switch_editors_view.vue';
-import eventHub from '~/ide/eventhub';
-import { createStore } from '~/ide/stores';
-
-jest.mock('~/flash');
-jest.mock('~/lib/logger');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-
-const TEST_USER_PREFERENCES_PATH = '/test/user-pref/path';
-const TEST_SWITCH_EDITOR_SVG_PATH = '/test/switch/editor/path.svg';
-const TEST_HREF = '/test/new/web/ide/href';
-
-describe('~/ide/components/switch_editors/switch_editors_view.vue', () => {
- useMockLocationHelper();
-
- let store;
- let wrapper;
- let confirmResolve;
- let requestSpy;
- let skipBeforeunloadSpy;
- let axiosMock;
-
- // region: finders ------------------
- const findButton = () => wrapper.findComponent(GlButton);
- const findEmptyState = () => wrapper.findComponent(GlEmptyState);
-
- // region: actions ------------------
- const triggerSwitchPreference = () => findButton().vm.$emit('click');
- const submitConfirm = async (val) => {
- confirmResolve(val);
-
- // why: We need to wait for promises for the immediate next lines to be executed
- await waitForPromises();
- };
-
- const createComponent = () => {
- wrapper = shallowMount(SwitchEditorsView, {
- store,
- stubs: {
- GlEmptyState,
- },
- });
- };
-
- // region: test setup ------------------
- beforeEach(() => {
- // Setup skip-beforeunload side-effect
- skipBeforeunloadSpy = jest.fn();
- eventHub.$on('skip-beforeunload', skipBeforeunloadSpy);
-
- // Setup request side-effect
- requestSpy = jest.fn().mockImplementation(() => new Promise(() => {}));
- axiosMock = new MockAdapter(axios);
- axiosMock.onPut(TEST_USER_PREFERENCES_PATH).reply(({ data }) => requestSpy(data));
-
- // Setup store
- store = createStore();
- store.state.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
- store.state.switchEditorSvgPath = TEST_SWITCH_EDITOR_SVG_PATH;
- store.state.links = {
- newWebIDEHelpPagePath: TEST_HREF,
- };
-
- // Setup user confirm side-effect
- confirmAction.mockImplementation(
- () =>
- new Promise((resolve) => {
- confirmResolve = resolve;
- }),
- );
- });
-
- afterEach(() => {
- eventHub.$off('skip-beforeunload', skipBeforeunloadSpy);
-
- axiosMock.restore();
- });
-
- // region: tests ------------------
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('render empty state', () => {
- expect(findEmptyState().props()).toMatchObject({
- svgPath: TEST_SWITCH_EDITOR_SVG_PATH,
- svgHeight: 150,
- title: MSG_TITLE,
- });
- });
-
- it('render link', () => {
- expect(wrapper.findComponent(GlLink).attributes('href')).toBe(TEST_HREF);
- expect(wrapper.findComponent(GlLink).text()).toBe(MSG_LEARN_MORE);
- });
-
- it('renders description', () => {
- expect(findEmptyState().text()).toContain(MSG_DESCRIPTION);
- });
-
- it('is not loading', () => {
- expect(findButton().props('loading')).toBe(false);
- });
- });
-
- describe('when user triggers switch preference', () => {
- beforeEach(() => {
- createComponent();
-
- triggerSwitchPreference();
- });
-
- it('creates a single confirm', () => {
- // Call again to ensure that we only show 1 confirm action
- triggerSwitchPreference();
-
- expect(confirmAction).toHaveBeenCalledTimes(1);
- expect(confirmAction).toHaveBeenCalledWith(MSG_CONFIRM, {
- primaryBtnText: __('Switch editors'),
- cancelBtnText: __('Cancel'),
- });
- });
-
- it('starts loading', () => {
- expect(findButton().props('loading')).toBe(true);
- });
-
- describe('when user cancels confirm', () => {
- beforeEach(async () => {
- await submitConfirm(false);
- });
-
- it('does not make request', () => {
- expect(requestSpy).not.toHaveBeenCalled();
- });
-
- it('can be triggered again', () => {
- triggerSwitchPreference();
-
- expect(confirmAction).toHaveBeenCalledTimes(2);
- });
- });
-
- describe('when user accepts confirm and response success', () => {
- beforeEach(async () => {
- requestSpy.mockReturnValue([200, {}]);
- await submitConfirm(true);
- });
-
- it('does not handle error', () => {
- expect(logError).not.toHaveBeenCalled();
- expect(createAlert).not.toHaveBeenCalled();
- });
-
- it('emits "skip-beforeunload" and reloads', () => {
- expect(skipBeforeunloadSpy).toHaveBeenCalledTimes(1);
- expect(window.location.reload).toHaveBeenCalledTimes(1);
- });
-
- it('calls request', () => {
- expect(requestSpy).toHaveBeenCalledTimes(1);
- expect(requestSpy).toHaveBeenCalledWith(
- JSON.stringify({ user: { use_legacy_web_ide: false } }),
- );
- });
-
- it('is not loading', () => {
- expect(findButton().props('loading')).toBe(false);
- });
- });
-
- describe('when user accepts confirm and response fails', () => {
- beforeEach(async () => {
- requestSpy.mockReturnValue([400, {}]);
- await submitConfirm(true);
- });
-
- it('handles error', () => {
- expect(logError).toHaveBeenCalledTimes(1);
- expect(logError).toHaveBeenCalledWith(
- 'Error while updating user preferences',
- expect.any(Error),
- );
-
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: MSG_ERROR_ALERT,
- });
- });
-
- it('does not reload', () => {
- expect(skipBeforeunloadSpy).not.toHaveBeenCalled();
- expect(window.location.reload).not.toHaveBeenCalled();
- });
- });
- });
-});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index 067da25cb52..97254ab680b 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -1,62 +1,190 @@
import { start } from '@gitlab/web-ide';
+import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from '~/ide/constants';
import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import { TEST_HOST } from 'helpers/test_constants';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('@gitlab/web-ide');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_action');
+jest.mock('~/lib/utils/create_and_submit_form');
+jest.mock('~/lib/utils/csrf', () => ({
+ token: 'mock-csrf-token',
+ headerKey: 'mock-csrf-header',
+}));
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
const TEST_PROJECT_PATH = 'group1/project1';
const TEST_BRANCH_NAME = '12345-foo-patch';
const TEST_GITLAB_URL = 'https://test-gitlab/';
+const TEST_USER_PREFERENCES_PATH = '/user/preferences';
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
+const TEST_FILE_PATH = 'foo/README.md';
+const TEST_MR_ID = '7';
+const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
+const TEST_FORK_INFO = { fork_path: '/forky' };
+const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
+const TEST_START_REMOTE_PARAMS = {
+ remoteHost: 'dev.example.gitlab.com/test',
+ remotePath: '/test/projects/f oo',
+ connectionToken: '123abc',
+};
describe('ide/init_gitlab_web_ide', () => {
+ let resolveConfirm;
+
const createRootElement = () => {
const el = document.createElement('div');
el.id = ROOT_ELEMENT_ID;
// why: We'll test that this class is removed later
- el.classList.add('ide-loading');
+ el.classList.add('test-class');
el.dataset.projectPath = TEST_PROJECT_PATH;
el.dataset.cspNonce = TEST_NONCE;
el.dataset.branchName = TEST_BRANCH_NAME;
+ el.dataset.ideRemotePath = TEST_IDE_REMOTE_PATH;
+ el.dataset.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
+ el.dataset.mergeRequest = TEST_MR_ID;
+ el.dataset.filePath = TEST_FILE_PATH;
document.body.append(el);
};
const findRootElement = () => document.getElementById(ROOT_ELEMENT_ID);
- const act = () => initGitlabWebIDE(findRootElement());
+ const createSubject = () => initGitlabWebIDE(findRootElement());
+ const triggerHandleStartRemote = (startRemoteParams) => {
+ const [, config] = start.mock.calls[0];
+
+ config.handleStartRemote(startRemoteParams);
+ };
beforeEach(() => {
process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH;
window.gon.gitlab_url = TEST_GITLAB_URL;
- createRootElement();
+ confirmAction.mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveConfirm = resolve;
+ }),
+ );
- act();
+ createRootElement();
});
afterEach(() => {
document.body.innerHTML = '';
});
- it('calls start with element', () => {
- expect(start).toHaveBeenCalledWith(findRootElement(), {
- baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
- projectPath: TEST_PROJECT_PATH,
- ref: TEST_BRANCH_NAME,
- gitlabUrl: TEST_GITLAB_URL,
- nonce: TEST_NONCE,
+ describe('default', () => {
+ beforeEach(() => {
+ createSubject();
+ });
+
+ it('calls start with element', () => {
+ expect(start).toHaveBeenCalledTimes(1);
+ expect(start).toHaveBeenCalledWith(findRootElement(), {
+ baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ projectPath: TEST_PROJECT_PATH,
+ ref: TEST_BRANCH_NAME,
+ filePath: TEST_FILE_PATH,
+ mrId: TEST_MR_ID,
+ mrTargetProject: '',
+ forkInfo: null,
+ gitlabUrl: TEST_GITLAB_URL,
+ nonce: TEST_NONCE,
+ httpHeaders: {
+ 'mock-csrf-header': 'mock-csrf-token',
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ links: {
+ userPreferences: TEST_USER_PREFERENCES_PATH,
+ feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
+ },
+ handleStartRemote: expect.any(Function),
+ });
+ });
+
+ it('clears classes and data from root element', () => {
+ const rootEl = findRootElement();
+
+ // why: Snapshot to test that the element was cleaned including `test-class`
+ expect(rootEl.outerHTML).toBe(
+ '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>',
+ );
+ });
+
+ describe('when handleStartRemote is triggered', () => {
+ beforeEach(() => {
+ triggerHandleStartRemote(TEST_START_REMOTE_PARAMS);
+ });
+
+ it('promts for confirm', () => {
+ expect(confirmAction).toHaveBeenCalledWith(expect.any(String), {
+ primaryBtnText: expect.any(String),
+ cancelBtnText: expect.any(String),
+ });
+ });
+
+ it('does not submit, when not confirmed', async () => {
+ resolveConfirm(false);
+
+ await waitForPromises();
+
+ expect(createAndSubmitForm).not.toHaveBeenCalled();
+ });
+
+ it('submits, when confirmed', async () => {
+ resolveConfirm(true);
+
+ await waitForPromises();
+
+ expect(createAndSubmitForm).toHaveBeenCalledWith({
+ url: '/-/ide/remote/dev.example.gitlab.com%2Ftest/test/projects/f%20oo',
+ data: {
+ connection_token: TEST_START_REMOTE_PARAMS.connectionToken,
+ return_url: window.location.href,
+ },
+ });
+ });
+ });
+ });
+
+ describe('when URL has target_project in query params', () => {
+ beforeEach(() => {
+ setWindowLocation(
+ `https://example.com/-/ide?target_project=${encodeURIComponent(TEST_MR_TARGET_PROJECT)}`,
+ );
+
+ createSubject();
+ });
+
+ it('includes mrTargetProject', () => {
+ expect(start).toHaveBeenCalledWith(
+ findRootElement(),
+ expect.objectContaining({
+ mrTargetProject: TEST_MR_TARGET_PROJECT,
+ }),
+ );
});
});
- it('clears classes and data from root element', () => {
- const rootEl = findRootElement();
+ describe('when forkInfo is in dataset', () => {
+ beforeEach(() => {
+ findRootElement().dataset.forkInfo = JSON.stringify(TEST_FORK_INFO);
- // why: Snapshot to test that `ide-loading` was removed and no other
- // artifacts are remaining.
- expect(rootEl.outerHTML).toBe(
- '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>',
- );
+ createSubject();
+ });
+
+ it('includes forkInfo', () => {
+ expect(start).toHaveBeenCalledWith(
+ findRootElement(),
+ expect.objectContaining({
+ forkInfo: TEST_FORK_INFO,
+ }),
+ );
+ });
});
});
diff --git a/spec/frontend/ide/lib/common/model_spec.js b/spec/frontend/ide/lib/common/model_spec.js
index 5d1623429c0..39c50f628c2 100644
--- a/spec/frontend/ide/lib/common/model_spec.js
+++ b/spec/frontend/ide/lib/common/model_spec.js
@@ -149,7 +149,6 @@ describe('Multi-file editor library model', () => {
model.updateOptions({ insertSpaces: true, someOption: 'some value' });
expect(model.options).toEqual({
- endOfLine: 0,
insertFinalNewline: true,
insertSpaces: true,
someOption: 'some value',
@@ -181,16 +180,12 @@ describe('Multi-file editor library model', () => {
describe('applyCustomOptions', () => {
it.each`
option | value | contentBefore | contentAfter
- ${'endOfLine'} | ${0} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
- ${'endOfLine'} | ${0} | ${'hello\r\nworld\r\n'} | ${'hello\nworld\n'}
- ${'endOfLine'} | ${1} | ${'hello\nworld\n'} | ${'hello\r\nworld\r\n'}
- ${'endOfLine'} | ${1} | ${'hello\r\nworld\r\n'} | ${'hello\r\nworld\r\n'}
${'insertFinalNewline'} | ${true} | ${'hello\nworld'} | ${'hello\nworld\n'}
${'insertFinalNewline'} | ${true} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
${'insertFinalNewline'} | ${false} | ${'hello\nworld'} | ${'hello\nworld'}
${'trimTrailingWhitespace'} | ${true} | ${'hello \t\nworld \t\n'} | ${'hello\nworld\n'}
- ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\nworld\n'}
- ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\nworld \t\n'}
+ ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\r\nworld\r\n'}
+ ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\r\nworld \t\r\n'}
`(
'correctly applies custom option $option=$value to content',
({ option, value, contentBefore, contentAfter }) => {
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
new file mode 100644
index 00000000000..4b4e96f3b41
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
@@ -0,0 +1,22 @@
+import { getBaseConfig } from '~/ide/lib/gitlab_web_ide/get_base_config';
+import { TEST_HOST } from 'helpers/test_constants';
+
+const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/gitlab-web-ide/public/path';
+const TEST_GITLAB_URL = 'https://gdk.test/';
+
+describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
+ it('returns base properties for @gitlab/web-ide config', () => {
+ // why: add trailing "/" to test that it gets removed
+ process.env.GITLAB_WEB_IDE_PUBLIC_PATH = `${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}/`;
+ window.gon.gitlab_url = TEST_GITLAB_URL;
+
+ // act
+ const actual = getBaseConfig();
+
+ // asset
+ expect(actual).toEqual({
+ baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ gitlabUrl: TEST_GITLAB_URL,
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js
new file mode 100644
index 00000000000..35cf41b31f5
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js
@@ -0,0 +1,32 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setupRootElement } from '~/ide/lib/gitlab_web_ide/setup_root_element';
+
+describe('~/ide/lib/gitlab_web_ide/setup_root_element', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <div id="ide-test-root" class="js-not-a-real-class">
+ <span>We are loading lots of stuff...</span>
+ </div>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ const findIDERoot = () => document.getElementById('ide-test-root');
+
+ it('has no children, has original ID, and classes', () => {
+ const result = setupRootElement(findIDERoot());
+
+ // why: Assert that the return element matches the new one found in the dom
+ // (implying a el.replaceWith...)
+ expect(result).toBe(findIDERoot());
+ expect(result).toMatchInlineSnapshot(`
+ <div
+ class="gl--flex-center gl-relative gl-h-full"
+ id="ide-test-root"
+ />
+ `);
+ });
+});
diff --git a/spec/frontend/ide/remote/index_spec.js b/spec/frontend/ide/remote/index_spec.js
new file mode 100644
index 00000000000..0f23b0a4e45
--- /dev/null
+++ b/spec/frontend/ide/remote/index_spec.js
@@ -0,0 +1,91 @@
+import { startRemote } from '@gitlab/web-ide';
+import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide';
+import { mountRemoteIDE } from '~/ide/remote';
+import { TEST_HOST } from 'helpers/test_constants';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+
+jest.mock('@gitlab/web-ide');
+jest.mock('~/ide/lib/gitlab_web_ide');
+
+const TEST_DATA = {
+ remoteHost: 'example.com:3443',
+ remotePath: 'test/path/gitlab',
+ cspNonce: 'just7some8noncense',
+ connectionToken: 'connectAtoken',
+ returnUrl: 'https://example.com/return',
+};
+
+const TEST_BASE_CONFIG = {
+ gitlabUrl: '/test/gitlab',
+};
+
+const TEST_RETURN_URL_SAME_ORIGIN = `${TEST_HOST}/foo/example`;
+
+describe('~/ide/remote/index', () => {
+ useMockLocationHelper();
+ const originalHref = window.location.href;
+
+ let el;
+ let rootEl;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ Object.entries(TEST_DATA).forEach(([key, value]) => {
+ el.dataset[key] = value;
+ });
+
+ // Stub setupRootElement so we can assert on return element
+ rootEl = document.createElement('div');
+ setupRootElement.mockReturnValue(rootEl);
+
+ // Stub getBaseConfig so we can assert
+ getBaseConfig.mockReturnValue(TEST_BASE_CONFIG);
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ mountRemoteIDE(el);
+ });
+
+ it('calls startRemote', () => {
+ expect(startRemote).toHaveBeenCalledWith(rootEl, {
+ ...TEST_BASE_CONFIG,
+ nonce: TEST_DATA.cspNonce,
+ connectionToken: TEST_DATA.connectionToken,
+ remoteAuthority: `/${TEST_DATA.remoteHost}`,
+ hostPath: `/${TEST_DATA.remotePath}`,
+ handleError: expect.any(Function),
+ handleClose: expect.any(Function),
+ });
+ });
+ });
+
+ describe.each`
+ returnUrl | fnName | reloadExpectation | hrefExpectation
+ ${TEST_DATA.returnUrl} | ${'handleError'} | ${1} | ${originalHref}
+ ${TEST_DATA.returnUrl} | ${'handleClose'} | ${1} | ${originalHref}
+ ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleClose'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN}
+ ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleError'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN}
+ ${''} | ${'handleClose'} | ${1} | ${originalHref}
+ `(
+ 'with returnUrl=$returnUrl and fn=$fnName',
+ ({ returnUrl, fnName, reloadExpectation, hrefExpectation }) => {
+ beforeEach(() => {
+ el.dataset.returnUrl = returnUrl;
+
+ mountRemoteIDE(el);
+ });
+
+ it('changes location', () => {
+ expect(window.location.reload).not.toHaveBeenCalled();
+
+ const [, config] = startRemote.mock.calls[0];
+
+ config[fnName]();
+
+ expect(window.location.reload).toHaveBeenCalledTimes(reloadExpectation);
+ expect(window.location.href).toBe(hrefExpectation);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 0fab828dfb3..5847e8e1518 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -6,7 +6,7 @@ import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.
import services from '~/ide/services';
import { query, mutate } from '~/ide/services/gql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
-import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
+import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { projectData } from '../mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
index fc00bd075e7..8d21088bcaf 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -10,7 +10,7 @@ import {
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/root';
const TEST_BRANCH_ID = 'main';
@@ -78,7 +78,7 @@ describe('IDE store terminal check actions', () => {
describe('receiveConfigCheckError', () => {
it('handles error response', () => {
- const status = httpStatus.UNPROCESSABLE_ENTITY;
+ const status = HTTP_STATUS_UNPROCESSABLE_ENTITY;
const payload = { response: { status } };
return testAction(
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index f48797415df..df365442c67 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -6,7 +6,7 @@ import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/termi
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -285,7 +285,7 @@ describe('IDE store terminal session controls actions', () => {
);
});
- [httpStatus.NOT_FOUND, httpStatus.UNPROCESSABLE_ENTITY].forEach((status) => {
+ [httpStatus.NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY].forEach((status) => {
it(`dispatches request and startSession on ${status}`, () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
index e8f375a70b5..2a802d6b4af 100644
--- a/spec/frontend/ide/stores/modules/terminal/messages_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
@@ -1,7 +1,7 @@
import { escape } from 'lodash';
import { TEST_HOST } from 'spec/test_constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import { sprintf } from '~/locale';
const TEST_HELP_URL = `${TEST_HOST}/help`;
@@ -9,7 +9,7 @@ const TEST_HELP_URL = `${TEST_HOST}/help`;
describe('IDE store terminal messages', () => {
describe('configCheckError', () => {
it('returns job error, with status UNPROCESSABLE_ENTITY', () => {
- const result = messages.configCheckError(httpStatus.UNPROCESSABLE_ENTITY, TEST_HELP_URL);
+ const result = messages.configCheckError(HTTP_STATUS_UNPROCESSABLE_ENTITY, TEST_HELP_URL);
expect(result).toBe(
sprintf(
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index b896437ecb2..31e097cfa7b 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -1,16 +1,61 @@
import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+
+Vue.use(VueApollo);
+
+const makeGroupMock = (fullPath) => ({
+ id: `gid://gitlab/Group/${fullPath}`,
+ fullPath,
+ name: fullPath,
+ visibility: 'public',
+ webUrl: `http://gdk.test:3000/groups/${fullPath}`,
+ __typename: 'Group',
+});
+
+const AVAILABLE_NAMESPACES = [
+ makeGroupMock('match1'),
+ makeGroupMock('unrelated'),
+ makeGroupMock('match2'),
+];
+
+const SEARCH_NAMESPACES_MOCK = Promise.resolve({
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ groups: {
+ nodes: AVAILABLE_NAMESPACES,
+ __typename: 'GroupConnection',
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespaces::UserNamespace/1',
+ fullPath: 'root',
+ __typename: 'Namespace',
+ },
+ __typename: 'UserCore',
+ },
+ },
+});
describe('Import entities group dropdown component', () => {
let wrapper;
let namespacesTracker;
const createComponent = (propsData) => {
+ const apolloProvider = createMockApollo([
+ [searchNamespacesWhereUserCanCreateProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
+ ]);
+
namespacesTracker = jest.fn();
wrapper = shallowMount(GroupDropdown, {
+ apolloProvider,
scopedSlots: {
default: namespacesTracker,
},
@@ -23,33 +68,30 @@ describe('Import entities group dropdown component', () => {
wrapper.destroy();
});
- it('passes namespaces from props to default slot', () => {
- const namespaces = [
- { id: 1, fullPath: 'ns1' },
- { id: 2, fullPath: 'ns2' },
- ];
- createComponent({ namespaces });
+ it('passes namespaces from graphql query to default slot', async () => {
+ createComponent();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await nextTick();
+ await waitForPromises();
+ await nextTick();
- expect(namespacesTracker).toHaveBeenCalledWith({ namespaces });
+ expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: AVAILABLE_NAMESPACES });
});
it('filters namespaces based on user input', async () => {
- const namespaces = [
- { id: 1, fullPath: 'match1' },
- { id: 2, fullPath: 'some unrelated' },
- { id: 3, fullPath: 'match2' },
- ];
- createComponent({ namespaces });
+ createComponent();
namespacesTracker.mockReset();
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
-
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await nextTick();
+ await waitForPromises();
await nextTick();
expect(namespacesTracker).toHaveBeenCalledWith({
namespaces: [
- { id: 1, fullPath: 'match1' },
- { id: 3, fullPath: 'match2' },
+ expect.objectContaining({ fullPath: 'match1' }),
+ expect.objectContaining({ fullPath: 'match2' }),
],
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index 61f860688dc..f7a97f22d44 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -15,8 +15,13 @@ import ImportTable from '~/import_entities/import_groups/components/import_table
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
-import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
+import {
+ AVAILABLE_NAMESPACES,
+ availableNamespacesFixture,
+ generateFakeEntry,
+} from '../graphql/fixtures';
jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/services/status_poller');
@@ -60,15 +65,22 @@ describe('import table', () => {
wrapper.findAll('tbody td input[type=checkbox]').at(idx).setChecked(true);
const createComponent = ({ bulkImportSourceGroups, importGroups, defaultTargetNamespace }) => {
- apolloProvider = createMockApollo([], {
- Query: {
- availableNamespaces: () => availableNamespacesFixture,
- bulkImportSourceGroups,
- },
- Mutation: {
- importGroups,
+ apolloProvider = createMockApollo(
+ [
+ [
+ searchNamespacesWhereUserCanCreateProjectsQuery,
+ () => Promise.resolve(availableNamespacesFixture),
+ ],
+ ],
+ {
+ Query: {
+ bulkImportSourceGroups,
+ },
+ Mutation: {
+ importGroups,
+ },
},
- });
+ );
wrapper = mount(ImportTable, {
propsData: {
@@ -173,7 +185,7 @@ describe('import table', () => {
});
it('respects default namespace if provided', async () => {
- const targetNamespace = availableNamespacesFixture[1];
+ const targetNamespace = AVAILABLE_NAMESPACES[1];
createComponent({
bulkImportSourceGroups: () => ({
@@ -227,7 +239,7 @@ describe('import table', () => {
{
newName: FAKE_GROUP.lastImportTarget.newName,
sourceGroupId: FAKE_GROUP.id,
- targetNamespace: availableNamespacesFixture[0].fullPath,
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
},
],
},
@@ -519,12 +531,12 @@ describe('import table', () => {
variables: {
importRequests: [
{
- targetNamespace: availableNamespacesFixture[0].fullPath,
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
newName: NEW_GROUPS[0].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[0].id,
},
{
- targetNamespace: availableNamespacesFixture[0].fullPath,
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
newName: NEW_GROUPS[1].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[1].id,
},
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index 18dc1217fec..d5286e71c44 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -1,9 +1,22 @@
import { GlDropdownItem, GlFormInput } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
-import { generateFakeEntry, availableNamespacesFixture } from '../graphql/fixtures';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+
+import {
+ generateFakeEntry,
+ availableNamespacesFixture,
+ AVAILABLE_NAMESPACES,
+} from '../graphql/fixtures';
+
+Vue.use(VueApollo);
const generateFakeTableEntry = ({ flags = {}, ...config }) => {
const entry = generateFakeEntry(config);
@@ -11,7 +24,7 @@ const generateFakeTableEntry = ({ flags = {}, ...config }) => {
return {
...entry,
importTarget: {
- targetNamespace: availableNamespacesFixture[0],
+ targetNamespace: AVAILABLE_NAMESPACES[0],
newName: entry.lastImportTarget.newName,
},
flags,
@@ -20,16 +33,24 @@ const generateFakeTableEntry = ({ flags = {}, ...config }) => {
describe('import target cell', () => {
let wrapper;
+ let apolloProvider;
let group;
const findNameInput = () => wrapper.findComponent(GlFormInput);
const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
const createComponent = (props) => {
+ apolloProvider = createMockApollo([
+ [
+ searchNamespacesWhereUserCanCreateProjectsQuery,
+ () => Promise.resolve(availableNamespacesFixture),
+ ],
+ ]);
+
wrapper = shallowMount(ImportTargetCell, {
+ apolloProvider,
stubs: { ImportGroupDropdown },
propsData: {
- availableNamespaces: availableNamespacesFixture,
groupPathRegex: /.*/,
...props,
},
@@ -42,9 +63,12 @@ describe('import target cell', () => {
});
describe('events', () => {
- beforeEach(() => {
+ beforeEach(async () => {
group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
createComponent({ group });
+ await nextTick();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await nextTick();
});
it('emits update-new-name when input value is changed', () => {
@@ -59,7 +83,9 @@ describe('import target cell', () => {
dropdownItem.vm.$emit('click');
expect(wrapper.emitted('update-target-namespace')).toBeDefined();
- expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(availableNamespacesFixture[1]);
+ expect(wrapper.emitted('update-target-namespace')[0][0]).toStrictEqual(
+ AVAILABLE_NAMESPACES[1],
+ );
});
});
@@ -94,18 +120,20 @@ describe('import target cell', () => {
expect(items).toHaveLength(1);
});
- it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => {
+ it('renders both no parent option and available namespaces list when available namespaces list is not empty', async () => {
createComponent({
group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
- availableNamespaces: availableNamespacesFixture,
});
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await waitForPromises();
+ await nextTick();
const [firstItem, ...rest] = findNamespaceDropdown()
.findAllComponents(GlDropdownItem)
.wrappers.map((w) => w.text());
expect(firstItem).toBe('No parent');
- expect(rest).toHaveLength(availableNamespacesFixture.length);
+ expect(rest).toHaveLength(AVAILABLE_NAMESPACES.length);
});
describe('when entity is not available for import', () => {
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index 52c868e5356..adc4ebcffb8 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -10,12 +10,11 @@ import {
import { LocalStorageCache } from '~/import_entities/import_groups/graphql/services/local_storage_cache';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
-import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
+import { statusEndpointFixture } from './fixtures';
jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({
@@ -28,7 +27,6 @@ jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache'
const FAKE_ENDPOINTS = {
status: '/fake_status_url',
- availableNamespaces: '/fake_available_namespaces',
createBulkImport: '/fake_create_bulk_import',
jobs: '/fake_jobs',
};
@@ -55,14 +53,6 @@ describe('Bulk import resolvers', () => {
client = createClient();
axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(
- httpStatus.OK,
- availableNamespacesFixture.map((ns) => ({
- id: ns.id,
- full_path: ns.fullPath,
- })),
- );
-
client.watchQuery({ query: bulkImportSourceGroupsQuery }).subscribe(({ data }) => {
results = data.bulkImportSourceGroups.nodes;
});
@@ -75,22 +65,6 @@ describe('Bulk import resolvers', () => {
});
describe('queries', () => {
- describe('availableNamespaces', () => {
- let namespacesResults;
- beforeEach(async () => {
- const response = await client.query({ query: availableNamespacesQuery });
- namespacesResults = response.data.availableNamespaces;
- });
-
- it('mirrors REST endpoint response fields', () => {
- const extractRelevantFields = (obj) => ({ id: obj.id, full_path: obj.full_path });
-
- expect(namespacesResults.map(extractRelevantFields)).toStrictEqual(
- availableNamespacesFixture.map(extractRelevantFields),
- );
- });
- });
-
describe('bulkImportSourceGroups', () => {
it('respects cached import state when provided by group manager', async () => {
const [localStorageCache] = LocalStorageCache.mock.instances;
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
index 938020e03f0..7530e9fc348 100644
--- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -59,9 +59,36 @@ export const statusEndpointFixture = {
},
};
-export const availableNamespacesFixture = Object.freeze([
- { id: 24, fullPath: 'Commit451' },
- { id: 22, fullPath: 'gitlab-org' },
- { id: 23, fullPath: 'gnuwget' },
- { id: 25, fullPath: 'jashkenas' },
-]);
+const makeGroupMock = ({ id, fullPath }) => ({
+ id,
+ fullPath,
+ name: fullPath,
+ visibility: 'public',
+ webUrl: `http://gdk.test:3000/groups/${fullPath}`,
+ __typename: 'Group',
+});
+
+export const AVAILABLE_NAMESPACES = [
+ makeGroupMock({ id: 24, fullPath: 'Commit451' }),
+ makeGroupMock({ id: 22, fullPath: 'gitlab-org' }),
+ makeGroupMock({ id: 23, fullPath: 'gnuwget' }),
+ makeGroupMock({ id: 25, fullPath: 'jashkenas' }),
+];
+
+export const availableNamespacesFixture = {
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ groups: {
+ nodes: AVAILABLE_NAMESPACES,
+ __typename: 'GroupConnection',
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespaces::UserNamespace/1',
+ fullPath: 'root',
+ __typename: 'Namespace',
+ },
+ __typename: 'UserCore',
+ },
+ },
+};
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 53807167fe8..51f82dab381 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -59,7 +59,6 @@ describe('ImportProjectsTable', () => {
actions: {
fetchRepos: fetchReposFn,
fetchJobs: jest.fn(),
- fetchNamespaces: jest.fn(),
importAll: importAllFn,
stopJobsPolling: jest.fn(),
clearJobsEtagPoll: jest.fn(),
@@ -95,12 +94,6 @@ describe('ImportProjectsTable', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- it('renders a loading icon while namespaces are loading', () => {
- createComponent({ state: { isLoadingNamespaces: true } });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
-
it('renders a table with provider repos', () => {
const repositories = [
{ importSource: { id: 1 }, importedProject: null },
@@ -214,35 +207,52 @@ describe('ImportProjectsTable', () => {
});
describe('when paginatable is set to true', () => {
- const pageInfo = { page: 1 };
+ const initState = {
+ namespaces: [{ fullPath: 'path' }],
+ pageInfo: { page: 1, hasNextPage: true },
+ repositories: [
+ { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
+ ],
+ };
+
+ describe('with hasNextPage true', () => {
+ beforeEach(() => {
+ createComponent({
+ state: initState,
+ paginatable: true,
+ });
+ });
- beforeEach(() => {
- createComponent({
- state: {
- namespaces: [{ fullPath: 'path' }],
- pageInfo,
- repositories: [
- { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
- ],
- },
- paginatable: true,
+ it('does not call fetchRepos on mount', () => {
+ expect(fetchReposFn).not.toHaveBeenCalled();
});
- });
- it('does not call fetchRepos on mount', () => {
- expect(fetchReposFn).not.toHaveBeenCalled();
- });
+ it('renders intersection observer component', () => {
+ expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
+ });
+
+ it('calls fetchRepos when intersection observer appears', async () => {
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
- it('renders intersection observer component', () => {
- expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
+ await nextTick();
+
+ expect(fetchReposFn).toHaveBeenCalled();
+ });
});
- it('calls fetchRepos when intersection observer appears', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
+ describe('with hasNextPage false', () => {
+ beforeEach(() => {
+ initState.pageInfo.hasNextPage = false;
- await nextTick();
+ createComponent({
+ state: initState,
+ paginatable: true,
+ });
+ });
- expect(fetchReposFn).toHaveBeenCalled();
+ it('does not render intersection observer component', () => {
+ expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 40934e90b78..d686036781f 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -10,13 +10,13 @@ import ProviderRepoTableRow from '~/import_entities/import_projects/components/p
describe('ProviderRepoTableRow', () => {
let wrapper;
const fetchImport = jest.fn();
+ const cancelImport = jest.fn();
const setImportTarget = jest.fn();
const fakeImportTarget = {
targetNamespace: 'target',
newName: 'newName',
};
- const availableNamespaces = ['test'];
const userNamespace = 'root';
function initStore(initialState) {
@@ -25,7 +25,7 @@ describe('ProviderRepoTableRow', () => {
getters: {
getImportTarget: () => () => fakeImportTarget,
},
- actions: { fetchImport, setImportTarget },
+ actions: { fetchImport, cancelImport, setImportTarget },
});
return store;
@@ -37,6 +37,14 @@ describe('ProviderRepoTableRow', () => {
return buttons.length ? buttons.at(0) : buttons;
};
+ const findCancelButton = () => {
+ const buttons = wrapper
+ .findAllComponents(GlButton)
+ .filter((node) => node.attributes('aria-label') === 'Cancel');
+
+ return buttons.length ? buttons.at(0) : buttons;
+ };
+
function mountComponent(props) {
Vue.use(Vuex);
@@ -44,7 +52,7 @@ describe('ProviderRepoTableRow', () => {
wrapper = shallowMount(ProviderRepoTableRow, {
store,
- propsData: { availableNamespaces, userNamespace, optionalStages: {}, ...props },
+ propsData: { userNamespace, optionalStages: {}, ...props },
});
}
@@ -78,9 +86,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a group namespace select', () => {
- expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe(
- availableNamespaces,
- );
+ expect(wrapper.findComponent(ImportGroupDropdown).exists()).toBe(true);
});
it('renders import button', () => {
@@ -113,6 +119,52 @@ describe('ProviderRepoTableRow', () => {
});
});
+ describe('when rendering importing project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ importStatus: STATUSES.STARTED,
+ },
+ };
+
+ describe('when cancelable is true', () => {
+ beforeEach(() => {
+ mountComponent({ repo, cancelable: true });
+ });
+
+ it('shows cancel button', () => {
+ expect(findCancelButton().isVisible()).toBe(true);
+ });
+
+ it('cancels import when clicking cancel button', async () => {
+ findCancelButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(cancelImport).toHaveBeenCalledWith(expect.anything(), {
+ repoId: repo.importSource.id,
+ });
+ });
+ });
+
+ describe('when cancelable is false', () => {
+ beforeEach(() => {
+ mountComponent({ repo, cancelable: false });
+ });
+
+ it('hides cancel button', () => {
+ expect(findCancelButton().isVisible()).toBe(false);
+ });
+ });
+ });
+
describe('when rendering imported project', () => {
const FAKE_STATS = {};
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index e154863f339..4b34c21daa3 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
-import { STATUSES } from '~/import_entities/constants';
+import { STATUSES, PROVIDERS } from '~/import_entities/constants';
import actionsFactory from '~/import_entities/import_projects/store/actions';
import { getImportTarget } from '~/import_entities/import_projects/store/getters';
import {
@@ -13,11 +13,10 @@ import {
RECEIVE_IMPORT_SUCCESS,
RECEIVE_IMPORT_ERROR,
RECEIVE_JOBS_SUCCESS,
- REQUEST_NAMESPACES,
- RECEIVE_NAMESPACES_SUCCESS,
- RECEIVE_NAMESPACES_ERROR,
+ CANCEL_IMPORT_SUCCESS,
SET_PAGE,
SET_FILTER,
+ SET_PAGE_CURSORS,
} from '~/import_entities/import_projects/store/mutation_types';
import state from '~/import_entities/import_projects/store/state';
import axios from '~/lib/utils/axios_utils';
@@ -30,7 +29,7 @@ const endpoints = {
reposPath: MOCK_ENDPOINT,
importPath: MOCK_ENDPOINT,
jobsPath: MOCK_ENDPOINT,
- namespacesPath: MOCK_ENDPOINT,
+ cancelPath: MOCK_ENDPOINT,
};
const {
@@ -39,8 +38,8 @@ const {
importAll,
fetchRepos,
fetchImport,
+ cancelImport,
fetchJobs,
- fetchNamespaces,
setFilter,
} = actionsFactory({
endpoints,
@@ -59,14 +58,17 @@ describe('import_projects store actions', () => {
...state(),
defaultTargetNamespace,
repositories: [
- { importSource: { id: importRepoId, sanitizedName }, importStatus: STATUSES.NONE },
+ {
+ importSource: { id: importRepoId, sanitizedName },
+ importedProject: { importStatus: STATUSES.NONE },
+ },
{
importSource: { id: otherImportRepoId, sanitizedName: 's2' },
- importStatus: STATUSES.NONE,
+ importedProject: { importStatus: STATUSES.NONE },
},
{
importSource: { id: 3, sanitizedName: 's3', incompatible: true },
- importStatus: STATUSES.NONE,
+ importedProject: { importStatus: STATUSES.NONE },
},
],
provider: 'provider',
@@ -77,7 +79,11 @@ describe('import_projects store actions', () => {
describe('fetchRepos', () => {
let mock;
- const payload = { imported_projects: [{}], provider_repos: [{}] };
+ const payload = {
+ imported_projects: [{}],
+ provider_repos: [{}],
+ page_info: { startCursor: 'start', endCursor: 'end', hasNextPage: true },
+ };
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -85,23 +91,53 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+ describe('with a successful request', () => {
+ it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, payload);
- return testAction(
- fetchRepos,
- null,
- localState,
- [
- { type: REQUEST_REPOS },
- { type: SET_PAGE, payload: 1 },
- {
- type: RECEIVE_REPOS_SUCCESS,
- payload: convertObjectPropsToCamelCase(payload, { deep: true }),
- },
- ],
- [],
- );
+ return testAction(
+ fetchRepos,
+ null,
+ localState,
+ [
+ { type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 1 },
+ {
+ type: RECEIVE_REPOS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+
+ describe('when provider is GITHUB_PROVIDER', () => {
+ beforeEach(() => {
+ localState.provider = PROVIDERS.GITHUB;
+ });
+
+ it('commits SET_PAGE_CURSORS instead of SET_PAGE', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+
+ return testAction(
+ fetchRepos,
+ null,
+ localState,
+ [
+ { type: REQUEST_REPOS },
+ {
+ type: SET_PAGE_CURSORS,
+ payload: { startCursor: 'start', endCursor: 'end', hasNextPage: true },
+ },
+ {
+ type: RECEIVE_REPOS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+ });
});
it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
@@ -116,18 +152,52 @@ describe('import_projects store actions', () => {
);
});
- it('includes page in url query params', async () => {
- let requestedUrl;
- mock.onGet().reply((config) => {
- requestedUrl = config.url;
- return [200, payload];
+ describe('with pagination params', () => {
+ it('includes page in url query params', async () => {
+ let requestedUrl;
+ mock.onGet().reply((config) => {
+ requestedUrl = config.url;
+ return [200, payload];
+ });
+
+ const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
+
+ await testAction(
+ fetchRepos,
+ null,
+ localStateWithPage,
+ expect.any(Array),
+ expect.any(Array),
+ );
+
+ expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
});
- const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
+ describe('when provider is "github"', () => {
+ beforeEach(() => {
+ localState.provider = PROVIDERS.GITHUB;
+ });
+
+ it('includes cursor in url query params', async () => {
+ let requestedUrl;
+ mock.onGet().reply((config) => {
+ requestedUrl = config.url;
+ return [200, payload];
+ });
- await testAction(fetchRepos, null, localStateWithPage, expect.any(Array), expect.any(Array));
+ const localStateWithPage = { ...localState, pageInfo: { endCursor: 'endTest' } };
- expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
+ await testAction(
+ fetchRepos,
+ null,
+ localStateWithPage,
+ expect.any(Array),
+ expect.any(Array),
+ );
+
+ expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?after=endTest`);
+ });
+ });
});
it('correctly keeps current page on an unsuccessful request', () => {
@@ -319,51 +389,6 @@ describe('import_projects store actions', () => {
});
});
- describe('fetchNamespaces', () => {
- let mock;
- const namespaces = [{ full_name: 'test/ns1' }, { full_name: 'test_ns2' }];
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => mock.restore());
-
- it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_SUCCESS on success', async () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, namespaces);
-
- await testAction(
- fetchNamespaces,
- null,
- localState,
- [
- { type: REQUEST_NAMESPACES },
- {
- type: RECEIVE_NAMESPACES_SUCCESS,
- payload: convertObjectPropsToCamelCase(namespaces, { deep: true }),
- },
- ],
- [],
- );
- });
-
- it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_ERROR and shows generic error message on an unsuccessful request', async () => {
- mock.onGet(MOCK_ENDPOINT).reply(500);
-
- await testAction(
- fetchNamespaces,
- null,
- localState,
- [{ type: REQUEST_NAMESPACES }, { type: RECEIVE_NAMESPACES_ERROR }],
- [],
- );
-
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Requesting namespaces failed',
- });
- });
- });
-
describe('importAll', () => {
it('dispatches multiple fetchImport actions', async () => {
const OPTIONAL_STAGES = { stage1: true, stage2: false };
@@ -398,4 +423,51 @@ describe('import_projects store actions', () => {
);
});
});
+
+ describe('cancelImport', () => {
+ let mock;
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('commits CANCEL_IMPORT_SUCCESS on success', async () => {
+ mock.onPost(MOCK_ENDPOINT).reply(200);
+
+ await testAction(
+ cancelImport,
+ { repoId: importRepoId },
+ localState,
+ [
+ {
+ type: CANCEL_IMPORT_SUCCESS,
+ payload: { repoId: 1 },
+ },
+ ],
+ [],
+ );
+ });
+
+ it('shows generic error message on an unsuccessful request', async () => {
+ mock.onPost(MOCK_ENDPOINT).reply(500);
+
+ await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Cancelling project import failed',
+ });
+ });
+
+ it('shows detailed error message on an unsuccessful request with errors fields in response', async () => {
+ const ERROR_MESSAGE = 'dummy';
+ mock.onPost(MOCK_ENDPOINT).reply(500, { errors: ERROR_MESSAGE });
+
+ await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Cancelling project import failed: ${ERROR_MESSAGE}`,
+ });
+ });
+ });
});
diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js
index 110b692b222..fced5670f25 100644
--- a/spec/frontend/import_entities/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js
@@ -1,6 +1,5 @@
import { STATUSES } from '~/import_entities/constants';
import {
- isLoading,
isImportingAnyRepo,
hasIncompatibleRepos,
hasImportableRepos,
@@ -31,24 +30,6 @@ describe('import_projects store getters', () => {
});
it.each`
- isLoadingRepos | isLoadingNamespaces | isLoadingValue
- ${false} | ${false} | ${false}
- ${true} | ${false} | ${true}
- ${false} | ${true} | ${true}
- ${true} | ${true} | ${true}
- `(
- 'isLoading returns $isLoadingValue when isLoadingRepos is $isLoadingRepos and isLoadingNamespaces is $isLoadingNamespaces',
- ({ isLoadingRepos, isLoadingNamespaces, isLoadingValue }) => {
- Object.assign(localState, {
- isLoadingRepos,
- isLoadingNamespaces,
- });
-
- expect(isLoading(localState)).toBe(isLoadingValue);
- },
- );
-
- it.each`
importStatus | value
${STATUSES.NONE} | ${false}
${STATUSES.SCHEDULING} | ${true}
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
index 77fae951300..7884e9b4307 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -27,7 +27,12 @@ describe('import_projects store mutations', () => {
state = {
filter: 'some-value',
repositories: ['some', ' repositories'],
- pageInfo: { page: 1 },
+ pageInfo: {
+ page: 1,
+ startCursor: 'Y3Vyc30yOjI2',
+ endCursor: 'Y3Vyc29yOjI1',
+ hasNextPage: false,
+ },
};
mutations[types.SET_FILTER](state, NEW_VALUE);
});
@@ -36,8 +41,11 @@ describe('import_projects store mutations', () => {
expect(state.repositories.length).toBe(0);
});
- it('resets current page to 0', () => {
+ it('resets pagintation', () => {
expect(state.pageInfo.page).toBe(0);
+ expect(state.pageInfo.startCursor).toBe(null);
+ expect(state.pageInfo.endCursor).toBe(null);
+ expect(state.pageInfo.hasNextPage).toBe(true);
});
});
@@ -263,43 +271,6 @@ describe('import_projects store mutations', () => {
});
});
- describe(`${types.REQUEST_NAMESPACES}`, () => {
- it('sets namespaces loading flag to true', () => {
- state = {};
-
- mutations[types.REQUEST_NAMESPACES](state);
-
- expect(state.isLoadingNamespaces).toBe(true);
- });
- });
-
- describe(`${types.RECEIVE_NAMESPACES_SUCCESS}`, () => {
- const response = [{ fullPath: 'some/path' }];
-
- beforeEach(() => {
- state = {};
- mutations[types.RECEIVE_NAMESPACES_SUCCESS](state, response);
- });
-
- it('stores namespaces to state', () => {
- expect(state.namespaces).toStrictEqual(response);
- });
-
- it('sets namespaces loading flag to false', () => {
- expect(state.isLoadingNamespaces).toBe(false);
- });
- });
-
- describe(`${types.RECEIVE_NAMESPACES_ERROR}`, () => {
- it('sets namespaces loading flag to false', () => {
- state = {};
-
- mutations[types.RECEIVE_NAMESPACES_ERROR](state);
-
- expect(state.isLoadingNamespaces).toBe(false);
- });
- });
-
describe(`${types.SET_IMPORT_TARGET}`, () => {
const PROJECT = {
id: 2,
@@ -345,4 +316,34 @@ describe('import_projects store mutations', () => {
expect(state.pageInfo.page).toBe(NEW_PAGE);
});
});
+
+ describe(`${types.SET_PAGE_CURSORS}`, () => {
+ it('sets page cursors', () => {
+ const NEW_CURSORS = { startCursor: 'startCur', endCursor: 'endCur', hasNextPage: false };
+ state = { pageInfo: { page: 1, startCursor: null, endCursor: null, hasNextPage: true } };
+
+ mutations[types.SET_PAGE_CURSORS](state, NEW_CURSORS);
+ expect(state.pageInfo).toEqual({ ...NEW_CURSORS, page: 1 });
+ });
+ });
+
+ describe(`${types.CANCEL_IMPORT_SUCCESS}`, () => {
+ const payload = { repoId: 1 };
+
+ beforeEach(() => {
+ state = {
+ repositories: [
+ {
+ importSource: { id: 1 },
+ importedProject: { importStatus: STATUSES.NONE },
+ },
+ ],
+ };
+ mutations[types.CANCEL_IMPORT_SUCCESS](state, payload);
+ });
+
+ it('updates project status', () => {
+ expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.CANCELED);
+ });
+ });
});
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 1b0253480e0..08c407cc4b4 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -1,5 +1,5 @@
import AxiosMockAdapter from 'axios-mock-adapter';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { ERROR_MSG } from '~/incidents_settings/constants';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
@@ -37,7 +37,7 @@ describe('IncidentsSettingsService', () => {
mock.onPatch().reply(httpStatusCodes.BAD_REQUEST);
return service.updateSettings({}).then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringContaining(ERROR_MSG),
});
});
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index 5af0e272285..7589b04b0fd 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -7,11 +7,16 @@ import { mockField } from '../mock_data';
describe('DynamicField', () => {
let wrapper;
- const createComponent = (props, isInheriting = false) => {
+ const createComponent = (props, isInheriting = false, editable = true) => {
wrapper = mount(DynamicField, {
propsData: { ...mockField, ...props },
computed: {
isInheriting: () => isInheriting,
+ propsSource: () => {
+ return {
+ editable,
+ };
+ },
},
});
};
@@ -28,12 +33,14 @@ describe('DynamicField', () => {
describe('template', () => {
describe.each`
- isInheriting | disabled | readonly | checkboxLabel
- ${true} | ${'disabled'} | ${'readonly'} | ${undefined}
- ${false} | ${undefined} | ${undefined} | ${'Custom checkbox label'}
+ isInheriting | editable | disabled | readonly | checkboxLabel
+ ${true} | ${true} | ${'disabled'} | ${'readonly'} | ${undefined}
+ ${false} | ${true} | ${undefined} | ${undefined} | ${'Custom checkbox label'}
+ ${true} | ${false} | ${'disabled'} | ${'readonly'} | ${undefined}
+ ${false} | ${false} | ${'disabled'} | ${undefined} | ${'Custom checkbox label'}
`(
- 'dynamic field, when isInheriting = `%p`',
- ({ isInheriting, disabled, readonly, checkboxLabel }) => {
+ 'dynamic field, when isInheriting = `$isInheriting` and editable = `$editable`',
+ ({ isInheriting, editable, disabled, readonly, checkboxLabel }) => {
describe('type is checkbox', () => {
beforeEach(() => {
createComponent(
@@ -42,6 +49,7 @@ describe('DynamicField', () => {
checkboxLabel,
},
isInheriting,
+ editable,
);
});
@@ -74,6 +82,7 @@ describe('DynamicField', () => {
],
},
isInheriting,
+ editable,
);
});
@@ -97,6 +106,7 @@ describe('DynamicField', () => {
type: 'textarea',
},
isInheriting,
+ editable,
);
});
@@ -119,6 +129,7 @@ describe('DynamicField', () => {
type: 'password',
},
isInheriting,
+ editable,
);
});
@@ -143,6 +154,7 @@ describe('DynamicField', () => {
required: true,
},
isInheriting,
+ editable,
);
});
@@ -204,7 +216,7 @@ describe('DynamicField', () => {
});
expect(findGlFormGroup().find('small').html()).toContain(
- '[<code>1</code> <a>3</a> <a href="foo">4</a>]',
+ '[<code>1</code> <a>3</a> <a href="foo" target="_blank" rel="noopener noreferrer">4</a>',
);
});
});
diff --git a/spec/frontend/integrations/edit/components/integration_form_actions_spec.js b/spec/frontend/integrations/edit/components/integration_form_actions_spec.js
new file mode 100644
index 00000000000..e95e30a1899
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/integration_form_actions_spec.js
@@ -0,0 +1,227 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
+import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
+import IntegrationFormActions from '~/integrations/edit/components/integration_form_actions.vue';
+
+import { integrationLevels } from '~/integrations/constants';
+import { createStore } from '~/integrations/edit/store';
+import { mockIntegrationProps } from '../mock_data';
+
+describe('IntegrationFormActions', () => {
+ let wrapper;
+
+ const createComponent = ({ customStateProps = {} } = {}) => {
+ const store = createStore({
+ customState: { ...mockIntegrationProps, ...customStateProps },
+ });
+ jest.spyOn(store, 'dispatch');
+
+ wrapper = shallowMountExtended(IntegrationFormActions, {
+ store,
+ propsData: {
+ hasSections: false,
+ },
+ });
+ };
+
+ const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
+ const findResetConfirmationModal = () => wrapper.findComponent(ResetConfirmationModal);
+ const findResetButton = () => wrapper.findByTestId('reset-button');
+ const findSaveButton = () => wrapper.findByTestId('save-button');
+ const findTestButton = () => wrapper.findByTestId('test-button');
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+
+ describe('ConfirmationModal', () => {
+ it.each`
+ desc | integrationLevel | shouldRender
+ ${'Should'} | ${integrationLevels.INSTANCE} | ${true}
+ ${'Should'} | ${integrationLevels.GROUP} | ${true}
+ ${'Should not'} | ${integrationLevels.PROJECT} | ${false}
+ `(
+ '$desc render the ConfirmationModal when integrationLevel is "$integrationLevel"',
+ ({ integrationLevel, shouldRender }) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ },
+ });
+ expect(findConfirmationModal().exists()).toBe(shouldRender);
+ },
+ );
+ });
+
+ describe('ResetConfirmationModal', () => {
+ it.each`
+ desc | integrationLevel | resetPath | shouldRender
+ ${'Should not'} | ${integrationLevels.INSTANCE} | ${''} | ${false}
+ ${'Should not'} | ${integrationLevels.GROUP} | ${''} | ${false}
+ ${'Should not'} | ${integrationLevels.PROJECT} | ${''} | ${false}
+ ${'Should'} | ${integrationLevels.INSTANCE} | ${'resetPath'} | ${true}
+ ${'Should'} | ${integrationLevels.GROUP} | ${'resetPath'} | ${true}
+ ${'Should not'} | ${integrationLevels.PROJECT} | ${'resetPath'} | ${false}
+ `(
+ '$desc render the ResetConfirmationModal modal when integrationLevel="$integrationLevel" and resetPath="$resetPath"',
+ ({ integrationLevel, resetPath, shouldRender }) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ resetPath,
+ },
+ });
+ expect(findResetConfirmationModal().exists()).toBe(shouldRender);
+ },
+ );
+ });
+
+ describe('Buttons rendering', () => {
+ it.each`
+ integrationLevel | canTest | resetPath | saveBtn | testBtn | cancelBtn | resetBtn
+ ${integrationLevels.PROJECT} | ${true} | ${'resetPath'} | ${true} | ${true} | ${true} | ${false}
+ ${integrationLevels.PROJECT} | ${false} | ${'resetPath'} | ${true} | ${false} | ${true} | ${false}
+ ${integrationLevels.PROJECT} | ${true} | ${''} | ${true} | ${true} | ${true} | ${false}
+ ${integrationLevels.GROUP} | ${true} | ${'resetPath'} | ${true} | ${true} | ${true} | ${true}
+ ${integrationLevels.GROUP} | ${false} | ${'resetPath'} | ${true} | ${false} | ${true} | ${true}
+ ${integrationLevels.GROUP} | ${true} | ${''} | ${true} | ${true} | ${true} | ${false}
+ ${integrationLevels.INSTANCE} | ${true} | ${'resetPath'} | ${true} | ${true} | ${true} | ${true}
+ ${integrationLevels.INSTANCE} | ${false} | ${'resetPath'} | ${true} | ${false} | ${true} | ${true}
+ ${integrationLevels.INSTANCE} | ${true} | ${''} | ${true} | ${true} | ${true} | ${false}
+ `(
+ 'on $integrationLevel when canTest="$canTest" and resetPath="$resetPath"',
+ ({ integrationLevel, canTest, resetPath, saveBtn, testBtn, cancelBtn, resetBtn }) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ canTest,
+ resetPath,
+ },
+ });
+
+ expect(findSaveButton().exists()).toBe(saveBtn);
+ expect(findTestButton().exists()).toBe(testBtn);
+ expect(findCancelButton().exists()).toBe(cancelBtn);
+ expect(findResetButton().exists()).toBe(resetBtn);
+ },
+ );
+ });
+
+ describe('interactions', () => {
+ describe('Save button clicked', () => {
+ const createAndSave = (integrationLevel, withModal = false) => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ canTest: true,
+ resetPath: 'resetPath',
+ },
+ });
+
+ findSaveButton().vm.$emit('click', new Event('click'));
+ if (withModal) {
+ findConfirmationModal().vm.$emit('submit');
+ }
+ wrapper.setProps({
+ isSaving: true,
+ });
+ };
+ const sharedFormStateTest = async (integrationLevel, withModal = false) => {
+ createAndSave(integrationLevel, withModal);
+
+ await nextTick();
+
+ const saveBtnWrapper = findSaveButton();
+ const testBtnWrapper = findTestButton();
+ const cancelBtnWrapper = findCancelButton();
+
+ expect(saveBtnWrapper.props('loading')).toBe(true);
+ expect(saveBtnWrapper.props('disabled')).toBe(true);
+
+ expect(testBtnWrapper.props('loading')).toBe(false);
+ expect(testBtnWrapper.props('disabled')).toBe(true);
+
+ expect(cancelBtnWrapper.props('loading')).toBe(false);
+ expect(cancelBtnWrapper.props('disabled')).toBe(true);
+ };
+
+ describe('on "project" level', () => {
+ const integrationLevel = integrationLevels.PROJECT;
+ it('emits the "save" event right away', async () => {
+ createAndSave(integrationLevel);
+ await nextTick();
+
+ expect(wrapper.emitted('save')).toHaveLength(1);
+ });
+
+ it('toggles the state of other buttons', async () => {
+ await sharedFormStateTest(integrationLevel);
+
+ const resetBtnWrapper = findResetButton();
+ expect(resetBtnWrapper.exists()).toBe(false);
+ });
+ });
+
+ describe.each([integrationLevels.INSTANCE, integrationLevels.GROUP])(
+ 'on "%s" level',
+ (integrationLevel) => {
+ it('emits the "save" event only after the confirmation', () => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ },
+ });
+
+ findSaveButton().vm.$emit('click', new Event('click'));
+ expect(wrapper.emitted('save')).toBeUndefined();
+
+ findConfirmationModal().vm.$emit('submit');
+ expect(wrapper.emitted('save')).toHaveLength(1);
+ });
+
+ it('toggles the state of other buttons', async () => {
+ await sharedFormStateTest(integrationLevel, true);
+
+ const resetBtnWrapper = findResetButton();
+ expect(resetBtnWrapper.props('loading')).toBe(false);
+ expect(resetBtnWrapper.props('disabled')).toBe(true);
+ });
+ },
+ );
+ });
+
+ describe('Reset button clicked', () => {
+ describe.each([integrationLevels.INSTANCE, integrationLevels.GROUP])(
+ 'on "%s" level',
+ (integrationLevel) => {
+ it('emits the "reset" event only after the confirmation', () => {
+ createComponent({
+ customStateProps: {
+ integrationLevel,
+ resetPath: 'resetPath',
+ },
+ });
+
+ findResetButton().vm.$emit('click', new Event('click'));
+ expect(wrapper.emitted('reset')).toBeUndefined();
+
+ findResetConfirmationModal().vm.$emit('reset');
+ expect(wrapper.emitted('reset')).toHaveLength(1);
+ });
+ },
+ );
+ });
+
+ describe('Test button clicked', () => {
+ it('emits the "test" event when clicked', () => {
+ createComponent({
+ customStateProps: {
+ integrationLevel: integrationLevels.PROJECT,
+ canTest: true,
+ },
+ });
+
+ findTestButton().vm.$emit('click', new Event('click'));
+ expect(wrapper.emitted('test')).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 7e67379f5ab..4b49e492880 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,21 +1,20 @@
import { GlAlert, GlBadge, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import * as Sentry from '@sentry/browser';
import { setHTMLFixture } from 'helpers/fixtures';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
-import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
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 IntegrationSectionConnection from '~/integrations/edit/components/sections/connection.vue';
+import IntegrationFormActions from '~/integrations/edit/components/integration_form_actions.vue';
import {
- integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
INTEGRATION_FORM_TYPE_SLACK,
@@ -60,7 +59,6 @@ describe('IntegrationForm', () => {
stubs: {
OverrideDropdown,
ActiveCheckbox,
- ConfirmationModal,
TriggerFields,
},
mocks: {
@@ -73,12 +71,6 @@ describe('IntegrationForm', () => {
const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown);
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
- const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
- const findResetConfirmationModal = () => wrapper.findComponent(ResetConfirmationModal);
- const findResetButton = () => wrapper.findByTestId('reset-button');
- const findProjectSaveButton = () => wrapper.findByTestId('save-button');
- const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
- const findTestButton = () => wrapper.findByTestId('test-button');
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findAlert = () => wrapper.findComponent(GlAlert);
const findGlBadge = () => wrapper.findComponent(GlBadge);
@@ -91,6 +83,7 @@ describe('IntegrationForm', () => {
const findConnectionSectionComponent = () =>
findConnectionSection().findComponent(IntegrationSectionConnection);
const findHelpHtml = () => wrapper.findByTestId('help-html');
+ const findFormActions = () => wrapper.findComponent(IntegrationFormActions);
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -102,108 +95,6 @@ describe('IntegrationForm', () => {
});
describe('template', () => {
- describe('integrationLevel is instance', () => {
- it('renders ConfirmationModal', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.INSTANCE,
- },
- });
-
- expect(findConfirmationModal().exists()).toBe(true);
- });
-
- describe('resetPath is empty', () => {
- it('does not render ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.INSTANCE,
- },
- });
-
- expect(findResetButton().exists()).toBe(false);
- expect(findResetConfirmationModal().exists()).toBe(false);
- });
- });
-
- describe('resetPath is present', () => {
- it('renders ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.INSTANCE,
- resetPath: 'resetPath',
- },
- });
-
- expect(findResetButton().exists()).toBe(true);
- expect(findResetConfirmationModal().exists()).toBe(true);
- });
- });
- });
-
- describe('integrationLevel is group', () => {
- it('renders ConfirmationModal', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- },
- });
-
- expect(findConfirmationModal().exists()).toBe(true);
- });
-
- describe('resetPath is empty', () => {
- it('does not render ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- },
- });
-
- expect(findResetButton().exists()).toBe(false);
- expect(findResetConfirmationModal().exists()).toBe(false);
- });
- });
-
- describe('resetPath is present', () => {
- it('renders ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- resetPath: 'resetPath',
- },
- });
-
- expect(findResetButton().exists()).toBe(true);
- expect(findResetConfirmationModal().exists()).toBe(true);
- });
- });
- });
-
- describe('integrationLevel is project', () => {
- it('does not render ConfirmationModal', () => {
- createComponent({
- customStateProps: {
- integrationLevel: 'project',
- },
- });
-
- expect(findConfirmationModal().exists()).toBe(false);
- });
-
- it('does not render ResetConfirmationModal and button', () => {
- createComponent({
- customStateProps: {
- integrationLevel: 'project',
- resetPath: 'resetPath',
- },
- });
-
- expect(findResetButton().exists()).toBe(false);
- expect(findResetConfirmationModal().exists()).toBe(false);
- });
- });
-
describe('triggerEvents is present', () => {
it('renders TriggerFields', () => {
const events = [{ title: 'push' }];
@@ -462,111 +353,85 @@ describe('IntegrationForm', () => {
);
});
- describe('when `save` button is clicked', () => {
- describe('buttons', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
- mountFn: mountExtended,
- });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
-
- it('sets save button `loading` prop to `true`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(true);
+ describe('Response to the "save" event (form submission)', () => {
+ const prepareComponentAndSave = async (initialActivated = true, checkValidityReturn) => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ initialActivated,
+ fields: [mockField],
+ },
+ mountFn: mountExtended,
});
+ jest.spyOn(findGlForm().element, 'submit');
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
- it('sets test button `disabled` prop to `true`', () => {
- expect(findTestButton().props('disabled')).toBe(true);
- });
- });
+ findFormActions().vm.$emit('save');
+ await nextTick();
+ };
- describe.each`
- checkValidityReturn | integrationActive
- ${true} | ${false}
- ${true} | ${true}
- ${false} | ${false}
+ it.each`
+ desc | checkValidityReturn | integrationActive | shouldSubmit
+ ${'form is valid'} | ${true} | ${false} | ${true}
+ ${'form is valid'} | ${true} | ${true} | ${true}
+ ${'form is invalid'} | ${false} | ${false} | ${true}
+ ${'form is invalid'} | ${false} | ${true} | ${false}
`(
- 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
- ({ integrationActive, checkValidityReturn }) => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: integrationActive,
- },
- mountFn: mountExtended,
- });
- jest.spyOn(findGlForm().element, 'submit');
- jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
+ 'when $desc (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
+ async ({ integrationActive, checkValidityReturn, shouldSubmit }) => {
+ await prepareComponentAndSave(integrationActive, checkValidityReturn);
- it('submit form', () => {
+ if (shouldSubmit) {
expect(findGlForm().element.submit).toHaveBeenCalledTimes(1);
- });
+ } else {
+ expect(findGlForm().element.submit).not.toHaveBeenCalled();
+ }
},
);
- describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- fields: [mockField],
- },
- mountFn: mountExtended,
- });
- jest.spyOn(findGlForm().element, 'submit');
- jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
-
- it('does not submit form', () => {
- expect(findGlForm().element.submit).not.toHaveBeenCalled();
- });
+ it('flips `isSaving` to `true`', async () => {
+ await prepareComponentAndSave(true, true);
+ expect(findFormActions().props('isSaving')).toBe(true);
+ });
- it('sets save button `loading` prop to `false`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(false);
+ describe('when form is invalid', () => {
+ beforeEach(async () => {
+ await prepareComponentAndSave(true, false);
});
- it('sets test button `disabled` prop to `false`', () => {
- expect(findTestButton().props('disabled')).toBe(false);
+ it('when form is invalid, it sets `isValidated` props on form fields', () => {
+ expect(findDynamicField().props('isValidated')).toBe(true);
});
- it('sets `isValidated` props on form fields', () => {
- expect(findDynamicField().props('isValidated')).toBe(true);
+ it('resets `isSaving`', () => {
+ expect(findFormActions().props('isSaving')).toBe(false);
});
});
});
- describe('when `test` button is clicked', () => {
+ describe('Response to the "test" event from the actions', () => {
describe('when form is invalid', () => {
- it('sets `isValidated` props on form fields', async () => {
+ beforeEach(async () => {
createComponent({
customStateProps: {
showActive: true,
- canTest: true,
fields: [mockField],
},
mountFn: mountExtended,
});
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
- await findTestButton().vm.$emit('click', new Event('click'));
+ findFormActions().vm.$emit('test');
+ await nextTick();
+ });
+ it('sets `isValidated` props on form fields', () => {
expect(findDynamicField().props('isValidated')).toBe(true);
});
+
+ it('resets `isTesting`', () => {
+ expect(findFormActions().props('isTesting')).toBe(false);
+ });
});
describe('when form is valid', () => {
@@ -576,26 +441,18 @@ describe('IntegrationForm', () => {
createComponent({
customStateProps: {
showActive: true,
- canTest: true,
testPath: mockTestPath,
},
mountFn: mountExtended,
});
+
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true);
});
- describe('buttons', () => {
- beforeEach(async () => {
- await findTestButton().vm.$emit('click', new Event('click'));
- });
-
- it('sets test button `loading` prop to `true`', () => {
- expect(findTestButton().props('loading')).toBe(true);
- });
-
- it('sets save button `disabled` prop to `true`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(true);
- });
+ it('flips `isTesting` to `true`', async () => {
+ findFormActions().vm.$emit('test');
+ await nextTick();
+ expect(findFormActions().props('isTesting')).toBe(true);
});
describe.each`
@@ -614,7 +471,7 @@ describe('IntegrationForm', () => {
service_response: serviceResponse,
});
- await findTestButton().vm.$emit('click', new Event('click'));
+ findFormActions().vm.$emit('test');
await waitForPromises();
});
@@ -622,14 +479,6 @@ describe('IntegrationForm', () => {
expect(mockToastShow).toHaveBeenCalledWith(expectToast);
});
- it('sets `loading` prop of test button to `false`', () => {
- expect(findTestButton().props('loading')).toBe(false);
- });
-
- it('sets save button `disabled` prop to `false`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(false);
- });
-
it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
});
@@ -638,44 +487,27 @@ describe('IntegrationForm', () => {
});
});
- describe('when `reset-confirmation-modal` emits `reset` event', () => {
+ describe('Response to the "reset" event from the actions', () => {
const mockResetPath = '/reset';
- describe('buttons', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- canTest: true,
- resetPath: mockResetPath,
- },
- });
-
- await findResetConfirmationModal().vm.$emit('reset');
+ beforeEach(async () => {
+ mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ createComponent({
+ customStateProps: {
+ resetPath: mockResetPath,
+ },
});
- it('sets reset button `loading` prop to `true`', () => {
- expect(findResetButton().props('loading')).toBe(true);
- });
+ findFormActions().vm.$emit('reset');
+ await nextTick();
+ });
- it('sets other button `disabled` props to `true`', () => {
- expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(true);
- expect(findTestButton().props('disabled')).toBe(true);
- });
+ it('flips `isResetting` to `true`', () => {
+ expect(findFormActions().props('isResetting')).toBe(true);
});
describe('when "reset settings" request fails', () => {
beforeEach(async () => {
- mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
- createComponent({
- customStateProps: {
- integrationLevel: integrationLevels.GROUP,
- canTest: true,
- resetPath: mockResetPath,
- },
- });
-
- await findResetConfirmationModal().vm.$emit('reset');
await waitForPromises();
});
@@ -687,13 +519,8 @@ describe('IntegrationForm', () => {
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
});
- it('sets reset button `loading` prop to `false`', () => {
- expect(findResetButton().props('loading')).toBe(false);
- });
-
- it('sets button `disabled` props to `false`', () => {
- expect(findInstanceOrGroupSaveButton().props('disabled')).toBe(false);
- expect(findTestButton().props('disabled')).toBe(false);
+ it('resets `isResetting`', () => {
+ expect(findFormActions().props('isResetting')).toBe(false);
});
});
@@ -702,64 +529,74 @@ describe('IntegrationForm', () => {
mockAxios.onPost(mockResetPath).replyOnce(httpStatus.OK);
createComponent({
customStateProps: {
- integrationLevel: integrationLevels.GROUP,
resetPath: mockResetPath,
},
});
- await findResetConfirmationModal().vm.$emit('reset');
+ findFormActions().vm.$emit('reset');
await waitForPromises();
});
it('calls `refreshCurrentPage`', () => {
expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
});
- });
- describe('Slack integration', () => {
- describe('Help and sections rendering', () => {
- const dummyHelp = 'Foo Help';
-
- it.each`
- integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
- ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
- `(
- '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
- ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
- createComponent({
- provide: {
- helpHtml,
- glFeatures: { integrationSlackAppNotifications: flagIsOn },
- },
- customStateProps: {
- sections,
- type: integration,
- },
- });
- expect(findAllSections().length > 0).toEqual(shouldShowSections);
- expect(findHelpHtml().exists()).toBe(shouldShowHelp);
- if (shouldShowHelp) {
- expect(findHelpHtml().html()).toContain(helpHtml);
- }
- },
- );
+ it('resets `isResetting`', async () => {
+ expect(findFormActions().props('isResetting')).toBe(false);
});
+ });
+ });
+
+ describe('Slack integration', () => {
+ describe('Help and sections rendering', () => {
+ const dummyHelp = 'Foo Help';
+
+ it.each`
+ integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
+ ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ `(
+ '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
+ ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ createComponent({
+ provide: {
+ helpHtml,
+ glFeatures: { integrationSlackAppNotifications: flagIsOn },
+ },
+ customStateProps: {
+ sections,
+ type: integration,
+ },
+ });
+ expect(findAllSections().length > 0).toEqual(shouldShowSections);
+ expect(findHelpHtml().exists()).toBe(shouldShowHelp);
+ if (shouldShowHelp) {
+ expect(findHelpHtml().html()).toContain(helpHtml);
+ }
+ },
+ );
+ });
+ describe.each`
+ hasSections | hasFieldsWithoutSections | description
+ ${true} | ${true} | ${'When having both: the sections and the fields without a section'}
+ ${true} | ${false} | ${'When having the sections only'}
+ ${false} | ${true} | ${'When having only the fields without a section'}
+ `('$description', ({ hasSections, hasFieldsWithoutSections }) => {
it.each`
prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert
${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true}
@@ -769,7 +606,7 @@ describe('IntegrationForm', () => {
${'does not'} | ${'foo'} | ${false} | ${true} | ${false}
${'does not'} | ${'foo'} | ${true} | ${false} | ${false}
`(
- '$prefix render the upgrade warnning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack"',
+ '$prefix render the upgrade warning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections',
({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => {
createComponent({
provide: {
@@ -778,7 +615,8 @@ describe('IntegrationForm', () => {
customStateProps: {
shouldUpgradeSlack,
type: integration,
- sections: [mockSectionConnection],
+ sections: hasSections ? [mockSectionConnection] : [],
+ fields: hasFieldsWithoutSections ? [mockField] : [],
},
});
expect(findAlert().exists()).toBe(shouldShowAlert);
diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
index 8b2d13be309..d839cde163c 100644
--- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
@@ -8,6 +8,12 @@ import * as ProjectsApi from '~/api/projects_api';
import ImportProjectMembersModal from '~/invite_members/components/import_project_members_modal.vue';
import ProjectSelect from '~/invite_members/components/project_select.vue';
import axios from '~/lib/utils/axios_utils';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
+
+jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
let wrapper;
let mock;
@@ -19,11 +25,12 @@ const $toast = {
show: jest.fn(),
};
-const createComponent = () => {
+const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(ImportProjectMembersModal, {
propsData: {
projectId,
projectName,
+ ...props,
},
stubs: {
GlModal: stubComponent(GlModal, {
@@ -101,6 +108,35 @@ describe('ImportProjectMembersModal', () => {
});
describe('submitting the import', () => {
+ describe('when the import is successful with reloadPageOnSubmit', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { reloadPageOnSubmit: true },
+ });
+
+ findProjectSelect().vm.$emit('input', projectToBeImported);
+
+ jest.spyOn(ProjectsApi, 'importProjectMembers').mockResolvedValue();
+
+ clickImportButton();
+ });
+
+ it('calls displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).toHaveBeenCalled();
+ });
+
+ it('calls reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).toHaveBeenCalled();
+ });
+
+ it('does not display the successful toastMessage', () => {
+ expect($toast.show).not.toHaveBeenCalledWith(
+ 'Successfully imported',
+ wrapper.vm.$options.toastOptions,
+ );
+ });
+ });
+
describe('when the import is successful', () => {
beforeEach(() => {
createComponent();
@@ -126,6 +162,14 @@ describe('ImportProjectMembersModal', () => {
);
});
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
+
it('sets isLoading to false after success', () => {
expect(findGlModal().props('actionPrimary').attributes.loading).toBe(false);
});
diff --git a/spec/frontend/invite_members/components/invite_group_notification_spec.js b/spec/frontend/invite_members/components/invite_group_notification_spec.js
new file mode 100644
index 00000000000..3e6ba6da9f4
--- /dev/null
+++ b/spec/frontend/invite_members/components/invite_group_notification_spec.js
@@ -0,0 +1,42 @@
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
+import { GROUP_MODAL_ALERT_BODY } from '~/invite_members/constants';
+
+describe('InviteGroupNotification', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(InviteGroupNotification, {
+ provide: { freeUsersLimit: 5 },
+ propsData: { name: 'name' },
+ stubs: { GlSprintf },
+ });
+ };
+
+ describe('when rendering', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes the correct props', () => {
+ expect(findAlert().props()).toMatchObject({ variant: 'warning', dismissible: false });
+ });
+
+ it('shows the correct message', () => {
+ const message = sprintf(GROUP_MODAL_ALERT_BODY, { count: 5 });
+
+ expect(findAlert().text()).toMatchInterpolatedText(message);
+ });
+
+ it('has a help link', () => {
+ expect(findLink().attributes('href')).toEqual(
+ 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index f9cb4a149f2..c2a55517405 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -6,9 +6,16 @@ import InviteGroupsModal from '~/invite_members/components/invite_groups_modal.v
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import GroupSelect from '~/invite_members/components/group_select.vue';
+import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
import { stubComponent } from 'helpers/stub_component';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
import { propsData, sharedGroup } from '../mock_data/group_modal';
+jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
+
describe('InviteGroupsModal', () => {
let wrapper;
@@ -44,6 +51,7 @@ describe('InviteGroupsModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findGroupSelect = () => wrapper.findComponent(GroupSelect);
+ const findInviteGroupAlert = () => wrapper.findComponent(InviteGroupNotification);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
@@ -74,6 +82,20 @@ describe('InviteGroupsModal', () => {
});
});
+ describe('rendering the invite group notification', () => {
+ it('shows the user limit notification alert when free user cap is enabled', () => {
+ createComponent({ freeUserCapEnabled: true });
+
+ expect(findInviteGroupAlert().exists()).toBe(true);
+ });
+
+ it('does not show the user limit notification alert', () => {
+ createComponent();
+
+ expect(findInviteGroupAlert().exists()).toBe(false);
+ });
+ });
+
describe('submitting the invite form', () => {
let apiResolve;
let apiReject;
@@ -126,6 +148,14 @@ describe('InviteGroupsModal', () => {
onComplete: expect.any(Function),
});
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
describe('when fails', () => {
@@ -156,4 +186,37 @@ describe('InviteGroupsModal', () => {
});
});
});
+
+ describe('submitting the invite form with reloadPageOnSubmit set true', () => {
+ const groupPostData = {
+ group_id: sharedGroup.id,
+ group_access: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ format: 'json',
+ };
+
+ beforeEach(() => {
+ createComponent({ reloadPageOnSubmit: true });
+ triggerGroupSelect(sharedGroup);
+
+ wrapper.vm.$toast = { show: jest.fn() };
+ jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
+
+ clickInviteButton();
+ });
+
+ describe('when succeeds', () => {
+ it('calls displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).toHaveBeenCalled();
+ });
+
+ it('calls reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).toHaveBeenCalled();
+ });
+
+ it('does not show the toast message on failure', () => {
+ expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 47be1933ed7..22fcedb2eaf 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -19,13 +19,17 @@ import {
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
EXPANDED_ERRORS,
- EMPTY_INVITES_ERROR_TEXT,
+ EMPTY_INVITES_ALERT_TEXT,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import {
propsData,
@@ -40,6 +44,7 @@ import {
GlEmoji,
} from '../mock_data/member_modal';
+jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
jest.mock('~/experimentation/experiment_tracking');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -57,6 +62,7 @@ describe('InviteMembersModal', () => {
},
propsData: {
usersLimitDataset: {},
+ fullPath: 'project',
...propsData,
...props,
},
@@ -95,6 +101,7 @@ describe('InviteMembersModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findBase = () => wrapper.findComponent(InviteModalBase);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
+ const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert');
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
@@ -397,7 +404,8 @@ describe('InviteMembersModal', () => {
await waitForPromises();
- expect(membersFormGroupInvalidFeedback()).toBe(EMPTY_INVITES_ERROR_TEXT);
+ expect(findEmptyInvitesAlert().text()).toBe(EMPTY_INVITES_ALERT_TEXT);
+ expect(membersFormGroupInvalidFeedback()).toBe(MEMBERS_PLACEHOLDER);
expect(findMembersSelect().props('exceptionState')).toBe(false);
await triggerMembersTokenSelect([user1]);
@@ -417,6 +425,29 @@ describe('InviteMembersModal', () => {
tasks_project_id: '',
};
+ describe('when reloadOnSubmit is true', () => {
+ beforeEach(async () => {
+ createComponent({ reloadPageOnSubmit: true });
+ await triggerMembersTokenSelect([user1, user2]);
+
+ wrapper.vm.$toast = { show: jest.fn() };
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
+ clickInviteButton();
+ });
+
+ it('calls displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).toHaveBeenCalled();
+ });
+
+ it('calls reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).toHaveBeenCalled();
+ });
+
+ it('does not show the toast message', () => {
+ expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ });
+ });
+
describe('when member is added successfully', () => {
beforeEach(async () => {
createComponent();
@@ -438,6 +469,14 @@ describe('InviteMembersModal', () => {
it('displays the successful toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
describe('when opened from a Learn GitLab page', () => {
@@ -464,7 +503,7 @@ describe('InviteMembersModal', () => {
describe('clearing the invalid state and message', () => {
beforeEach(async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_TAKEN);
clickInviteButton();
@@ -523,7 +562,7 @@ describe('InviteMembersModal', () => {
});
it('displays the restricted user api message for response with bad request', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton();
@@ -536,7 +575,7 @@ describe('InviteMembersModal', () => {
});
it('displays all errors when there are multiple existing users that are restricted by email', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@@ -590,6 +629,14 @@ describe('InviteMembersModal', () => {
it('displays the successful toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
});
@@ -633,7 +680,7 @@ describe('InviteMembersModal', () => {
});
it('displays the restricted email error when restricted email is invited', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
clickInviteButton();
@@ -647,7 +694,7 @@ describe('InviteMembersModal', () => {
});
it('displays all errors when there are multiple emails that return a restricted error message', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
@@ -677,6 +724,14 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('exceptionState')).toBe(false);
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
describe('when multiple emails are invited at the same time', () => {
@@ -698,7 +753,7 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
await triggerMembersTokenSelect([user3, user4, user5, user6]);
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EXPANDED_RESTRICTED);
clickInviteButton();
@@ -791,6 +846,14 @@ describe('InviteMembersModal', () => {
it('displays the successful toastMessage', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added');
});
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
it('calls Apis with the invite source passed through to openModal', async () => {
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index aeead8809fd..db2afbbd141 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -1,16 +1,15 @@
import {
- GlDropdown,
- GlDropdownItem,
+ GlFormSelect,
GlDatepicker,
GlFormGroup,
- GlSprintf,
GlLink,
+ GlSprintf,
GlModal,
GlIcon,
} from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -26,24 +25,30 @@ import { propsData, membersPath, purchasePath } from '../mock_data/modal_base';
describe('InviteModalBase', () => {
let wrapper;
- const createComponent = (props = {}, stubs = {}) => {
- wrapper = shallowMountExtended(InviteModalBase, {
+ const createComponent = ({ props = {}, stubs = {}, mountFn = shallowMountExtended } = {}) => {
+ const requiredStubs =
+ mountFn === mountExtended
+ ? {}
+ : {
+ ContentTransition,
+ GlFormSelect: true,
+ GlSprintf,
+ GlFormGroup: stubComponent(GlFormGroup, {
+ props: ['state', 'invalidFeedback'],
+ }),
+ };
+
+ wrapper = mountFn(InviteModalBase, {
propsData: {
...propsData,
...props,
},
stubs: {
- ContentTransition,
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
}),
- GlDropdown: true,
- GlDropdownItem: true,
- GlSprintf,
- GlFormGroup: stubComponent(GlFormGroup, {
- props: ['state', 'invalidFeedback'],
- }),
+ ...requiredStubs,
...stubs,
},
});
@@ -51,11 +56,10 @@ describe('InviteModalBase', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
+ const findFormSelect = () => wrapper.findComponent(GlFormSelect);
+ const findFormSelectOptions = () => findFormSelect().findAllComponents('option');
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIcon = () => wrapper.findComponent(GlIcon);
@@ -97,16 +101,29 @@ describe('InviteModalBase', () => {
});
describe('rendering the access levels dropdown', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ });
+ });
+
it('sets the default dropdown text to the default access level name', () => {
- expect(findDropdown().attributes('text')).toBe('Guest');
+ expect(findFormSelect().exists()).toBe(true);
+ expect(findFormSelect().element.value).toBe('10');
});
it('renders dropdown items for each accessLevel', () => {
- expect(findDropdownItems()).toHaveLength(5);
+ expect(findFormSelectOptions()).toHaveLength(5);
});
});
describe('rendering the help link', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ });
+ });
+
it('renders the correct link', () => {
expect(findLink().attributes('href')).toBe(propsData.helpLink);
});
@@ -126,7 +143,7 @@ describe('InviteModalBase', () => {
});
it('renders description', () => {
- createComponent({}, { GlFormGroup });
+ createComponent({ stubs: { GlFormGroup } });
expect(findMembersFormGroup().attributes('description')).toContain(
propsData.formGroupDescription,
@@ -144,13 +161,16 @@ describe('InviteModalBase', () => {
beforeEach(() => {
createComponent(
- { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } },
- { GlModal, GlFormGroup },
+ { props: { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } } },
+ { stubs: { GlModal, GlFormGroup } },
);
});
it('tracks actions', () => {
- createComponent({ usersLimitDataset: { reachedLimit: true } }, { GlFormGroup, GlModal });
+ createComponent({
+ props: { usersLimitDataset: { reachedLimit: true } },
+ stubs: { GlFormGroup, GlModal },
+ });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
const modal = wrapper.findComponent(GlModal);
@@ -164,8 +184,8 @@ describe('InviteModalBase', () => {
describe('when user limit is close on a personal namespace', () => {
beforeEach(() => {
- createComponent(
- {
+ createComponent({
+ props: {
usersLimitDataset: {
membersPath,
userNamespace: true,
@@ -173,8 +193,8 @@ describe('InviteModalBase', () => {
reachedLimit: false,
},
},
- { GlModal, GlFormGroup },
- );
+ stubs: { GlModal, GlFormGroup },
+ });
});
it('renders correct buttons', () => {
@@ -190,16 +210,16 @@ describe('InviteModalBase', () => {
});
describe('when users limit is not reached', () => {
- const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/;
+ const textRegex = /Select a role\s*Read more about role permissions\s*Access expiration date \(optional\)/;
beforeEach(() => {
- createComponent({ reachedLimit: false }, { GlModal, GlFormGroup });
+ createComponent({ props: { reachedLimit: false }, stubs: { GlModal, GlFormGroup } });
});
it('renders correct blocks', () => {
expect(findIcon().exists()).toBe(false);
expect(findDisabledInput().exists()).toBe(false);
- expect(findDropdown().exists()).toBe(true);
+ expect(findFormSelect().exists()).toBe(true);
expect(findDatepicker().exists()).toBe(true);
expect(wrapper.findComponent(GlModal).text()).toMatch(textRegex);
});
@@ -213,7 +233,9 @@ describe('InviteModalBase', () => {
it('with isLoading, shows loading for invite button', () => {
createComponent({
- isLoading: true,
+ props: {
+ isLoading: true,
+ },
});
expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
@@ -221,7 +243,9 @@ describe('InviteModalBase', () => {
it('with invalidFeedbackMessage, set members form group exception state', () => {
createComponent({
- invalidFeedbackMessage: 'invalid message!',
+ props: {
+ invalidFeedbackMessage: 'invalid message!',
+ },
});
expect(findMembersFormGroup().props()).toEqual({
diff --git a/spec/frontend/invite_members/mock_data/group_modal.js b/spec/frontend/invite_members/mock_data/group_modal.js
index c8588683885..65e8b025dd9 100644
--- a/spec/frontend/invite_members/mock_data/group_modal.js
+++ b/spec/frontend/invite_members/mock_data/group_modal.js
@@ -7,6 +7,8 @@ export const propsData = {
accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
defaultAccessLevel: 10,
helpLink: 'https://example.com',
+ fullPath: 'project',
+ freeUserCapEnabled: false,
};
export const sharedGroup = { id: '981' };
diff --git a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
new file mode 100644
index 00000000000..38b16dd0c2c
--- /dev/null
+++ b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
@@ -0,0 +1,54 @@
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '~/invite_members/utils/trigger_successful_invite_alert';
+import {
+ TOAST_MESSAGE_LOCALSTORAGE_KEY,
+ TOAST_MESSAGE_SUCCESSFUL,
+} from '~/invite_members/constants';
+import { createAlert } from '~/flash';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
+jest.mock('~/flash');
+useLocalStorageSpy();
+
+describe('Display Successful Invitation Alert', () => {
+ it('does not show alert if localStorage key not present', () => {
+ localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY);
+
+ displaySuccessfulInvitationAlert();
+
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
+ it('shows alert when localStorage key is present', () => {
+ localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
+
+ displaySuccessfulInvitationAlert();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: TOAST_MESSAGE_SUCCESSFUL,
+ variant: 'info',
+ });
+ });
+});
+
+describe('Reload On Invitation Success', () => {
+ const { location } = window;
+
+ beforeAll(() => {
+ delete window.location;
+ window.location = { reload: jest.fn() };
+ });
+
+ afterAll(() => {
+ window.location = location;
+ });
+
+ it('sets localStorage value and calls window.location.reload', () => {
+ reloadOnInvitationSuccess();
+
+ expect(localStorage.setItem).toHaveBeenCalledWith(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 3f72396cce6..3f40772f7fc 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -1,58 +1,380 @@
import { GlEmptyState } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { cloneDeep } from 'lodash';
+import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ filteredTokens,
+ locationSearch,
+ setSortPreferenceMutationResponse,
+ setSortPreferenceMutationResponseWithErrors,
+} from 'jest/issues/list/mock_data';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
+import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import { getSortKey, getSortOptions } from '~/issues/list/utils';
+import axios from '~/lib/utils/axios_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import {
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
+import { emptyIssuesQueryResponse, issuesQueryResponse } from '../mock_data';
+
+jest.mock('@sentry/browser');
+jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('IssuesDashboardApp component', () => {
+ let axiosMock;
let wrapper;
+ Vue.use(VueApollo);
+
const defaultProvide = {
calendarPath: 'calendar/path',
emptyStateSvgPath: 'empty-state.svg',
+ hasBlockedIssuesFeature: true,
+ hasIssuableHealthStatusFeature: true,
+ hasIssueWeightsFeature: true,
+ hasScopedLabelsFeature: true,
+ initialSort: CREATED_DESC,
+ isPublicVisibilityRestricted: false,
isSignedIn: true,
rssPath: 'rss/path',
};
+ let defaultQueryResponse = issuesQueryResponse;
+ if (IS_EE) {
+ defaultQueryResponse = cloneDeep(issuesQueryResponse);
+ defaultQueryResponse.data.issues.nodes[0].blockingCount = 1;
+ defaultQueryResponse.data.issues.nodes[0].healthStatus = null;
+ defaultQueryResponse.data.issues.nodes[0].weight = 5;
+ }
+
const findCalendarButton = () =>
wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.calendarButtonText });
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
+ const findIssueCardTimeInfo = () => wrapper.findComponent(IssueCardTimeInfo);
const findRssButton = () =>
wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText });
- const mountComponent = () => {
- wrapper = mountExtended(IssuesDashboardApp, { provide: defaultProvide });
+ const mountComponent = ({
+ provide = {},
+ issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse),
+ sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
+ } = {}) => {
+ wrapper = mountExtended(IssuesDashboardApp, {
+ apolloProvider: createMockApollo([
+ [getIssuesQuery, issuesQueryHandler],
+ [setSortPreferenceMutation, sortPreferenceMutationResponse],
+ ]),
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ });
};
beforeEach(() => {
- mountComponent();
+ setWindowLocation(TEST_HOST);
+ axiosMock = new AxiosMockAdapter(axios);
});
- it('renders IssuableList component', () => {
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ it('renders IssuableList component', async () => {
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
expect(findIssuableList().props()).toMatchObject({
currentTab: IssuableStates.Opened,
+ hasNextPage: true,
+ hasPreviousPage: false,
+ hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
+ initialSortBy: CREATED_DESC,
+ issuables: issuesQueryResponse.data.issues.nodes,
+ issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ showPaginationControls: true,
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
tabs: IssuesDashboardApp.IssuableListTabs,
+ urlParams: {
+ sort: urlSortParams[CREATED_DESC],
+ state: IssuableStates.Opened,
+ },
+ useKeysetPagination: true,
});
});
it('renders RSS button link', () => {
+ mountComponent();
+
expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
expect(findRssButton().props('icon')).toBe('rss');
});
it('renders calendar button link', () => {
+ mountComponent();
+
expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
expect(findCalendarButton().props('icon')).toBe('calendar');
});
- it('renders empty state', () => {
+ it('renders issue time information', async () => {
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ expect(findIssueCardTimeInfo().exists()).toBe(true);
+ });
+
+ it('renders issue statistics', async () => {
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+
+ expect(findIssueCardStatistics().exists()).toBe(true);
+ });
+
+ it('renders empty state', async () => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse) });
+ await waitForPromises();
+
expect(findEmptyState().props()).toMatchObject({
svgPath: defaultProvide.emptyStateSvgPath,
title: IssuesDashboardApp.i18n.emptyStateTitle,
});
});
+
+ describe('initial url params', () => {
+ describe('search', () => {
+ it('is set from the url params', () => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+
+ expect(findIssuableList().props('urlParams')).toMatchObject({ search: 'find issues' });
+ });
+ });
+
+ describe('sort', () => {
+ describe('when initial sort value uses old enum values', () => {
+ const oldEnumSortValues = Object.values(urlSortParams);
+
+ it.each(oldEnumSortValues)('initial sort is set with value %s', (sort) => {
+ mountComponent({ provide: { initialSort: sort } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(getSortKey(sort));
+ });
+ });
+
+ describe('when initial sort value uses new GraphQL enum values', () => {
+ const graphQLEnumSortValues = Object.keys(urlSortParams);
+
+ it.each(graphQLEnumSortValues)('initial sort is set with value %s', (sort) => {
+ mountComponent({ provide: { initialSort: sort.toLowerCase() } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(sort);
+ });
+ });
+
+ describe('when initial sort value is invalid', () => {
+ it.each(['', 'asdf', null, undefined])(
+ 'initial sort is set to value CREATED_DESC',
+ (sort) => {
+ mountComponent({ provide: { initialSort: sort } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
+ },
+ );
+ });
+ });
+
+ describe('state', () => {
+ it('is set from the url params', () => {
+ const initialState = IssuableStates.All;
+ setWindowLocation(`?state=${initialState}`);
+ mountComponent();
+
+ expect(findIssuableList().props('currentTab')).toBe(initialState);
+ });
+ });
+
+ describe('filter tokens', () => {
+ it('is set from the url params', () => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+
+ expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
+ });
+ });
+ });
+
+ describe('when there is an error fetching issues', () => {
+ beforeEach(() => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(i18n.errorFetchingIssues);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
+
+ it('clears error message when "dismiss-alert" event is emitted from IssuableList', async () => {
+ findIssuableList().vm.$emit('dismiss-alert');
+ await nextTick();
+
+ expect(findIssuableList().props('error')).toBeNull();
+ });
+ });
+
+ describe('tokens', () => {
+ const mockCurrentUser = {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'avatar/url',
+ };
+ const originalGon = window.gon;
+
+ beforeEach(() => {
+ window.gon = {
+ ...originalGon,
+ current_user_id: mockCurrentUser.id,
+ current_user_fullname: mockCurrentUser.name,
+ current_username: mockCurrentUser.username,
+ current_user_avatar_url: mockCurrentUser.avatar_url,
+ };
+ mountComponent();
+ });
+
+ afterEach(() => {
+ window.gon = originalGon;
+ });
+
+ it('renders all tokens alphabetically', () => {
+ const preloadedUsers = [{ ...mockCurrentUser, id: mockCurrentUser.id }];
+
+ expect(findIssuableList().props('searchTokens')).toMatchObject([
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ ]);
+ });
+ });
+
+ describe('events', () => {
+ describe('when "click-tab" event is emitted by IssuableList', () => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
+ });
+
+ it('updates ui to the new tab', () => {
+ expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
+ });
+
+ it('updates url to the new tab', () => {
+ expect(findIssuableList().props('urlParams')).toMatchObject({
+ state: IssuableStates.Closed,
+ });
+ });
+ });
+
+ describe.each(['next-page', 'previous-page'])(
+ 'when "%s" event is emitted by IssuableList',
+ (event) => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit(event);
+ });
+
+ it('scrolls to the top', () => {
+ expect(scrollUp).toHaveBeenCalled();
+ });
+ },
+ );
+
+ describe('when "sort" event is emitted by IssuableList', () => {
+ it.each(Object.keys(urlSortParams))(
+ 'updates to the new sort when payload is `%s`',
+ async (sortKey) => {
+ // Ensure initial sort key is different so we can trigger an update when emitting a sort key
+ if (sortKey === CREATED_DESC) {
+ mountComponent({ provide: { initialSort: UPDATED_DESC } });
+ } else {
+ mountComponent();
+ }
+
+ findIssuableList().vm.$emit('sort', sortKey);
+ await nextTick();
+
+ expect(findIssuableList().props('urlParams')).toMatchObject({
+ sort: urlSortParams[sortKey],
+ });
+ },
+ );
+
+ describe('when user is signed in', () => {
+ it('calls mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } });
+ });
+
+ it('captures error when mutation response has errors', async () => {
+ const mutationMock = jest
+ .fn()
+ .mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
+ mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
+ });
+ });
+
+ describe('when user is signed out', () => {
+ it('does not call mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ mountComponent({
+ provide: { isSignedIn: false },
+ sortPreferenceMutationResponse: mutationMock,
+ });
+
+ findIssuableList().vm.$emit('sort', CREATED_DESC);
+
+ expect(mutationMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/issues/dashboard/mock_data.js b/spec/frontend/issues/dashboard/mock_data.js
new file mode 100644
index 00000000000..feb4cb80bd8
--- /dev/null
+++ b/spec/frontend/issues/dashboard/mock_data.js
@@ -0,0 +1,88 @@
+export const issuesQueryResponse = {
+ data: {
+ issues: {
+ nodes: [
+ {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/123456',
+ iid: '789',
+ closedAt: null,
+ confidential: false,
+ createdAt: '2021-05-22T04:08:01Z',
+ downvotes: 2,
+ dueDate: '2021-05-29',
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: false,
+ moved: false,
+ reference: 'group/project#123456',
+ state: 'opened',
+ title: 'Issue title',
+ type: 'issue',
+ updatedAt: '2021-05-22T04:08:01Z',
+ upvotes: 3,
+ userDiscussionsCount: 4,
+ webPath: 'project/-/issues/789',
+ webUrl: 'project/-/issues/789',
+ assignees: {
+ nodes: [
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/234',
+ avatarUrl: 'avatar/url',
+ name: 'Marge Simpson',
+ username: 'msimpson',
+ webUrl: 'url/msimpson',
+ },
+ ],
+ },
+ author: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/456',
+ avatarUrl: 'avatar/url',
+ name: 'Homer Simpson',
+ username: 'hsimpson',
+ webUrl: 'url/hsimpson',
+ },
+ labels: {
+ nodes: [
+ {
+ id: 'gid://gitlab/ProjectLabel/456',
+ color: '#333',
+ title: 'Label title',
+ description: 'Label description',
+ },
+ ],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ completedCount: 1,
+ count: 2,
+ },
+ },
+ ],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ },
+ },
+};
+
+export const emptyIssuesQueryResponse = {
+ data: {
+ issues: {
+ nodes: [],
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ },
+ },
+};
diff --git a/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
new file mode 100644
index 00000000000..d0d20ef03e1
--- /dev/null
+++ b/spec/frontend/issues/list/components/empty_state_with_any_issues_spec.js
@@ -0,0 +1,68 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
+import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
+
+describe('EmptyStateWithAnyIssues component', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ emptyStateSvgPath: 'empty/state/svg/path',
+ newIssuePath: 'new/issue/path',
+ showNewIssueLink: false,
+ };
+
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const mountComponent = (props = {}) => {
+ wrapper = shallowMount(EmptyStateWithAnyIssues, {
+ propsData: {
+ hasSearch: true,
+ isOpenTab: true,
+ ...props,
+ },
+ provide: defaultProvide,
+ });
+ };
+
+ describe('when there is a search (with no results)', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: true });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ description: IssuesListApp.i18n.noSearchResultsDescription,
+ title: IssuesListApp.i18n.noSearchResultsTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+
+ describe('when "Open" tab is active', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: false, isOpenTab: true });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ description: IssuesListApp.i18n.noOpenIssuesDescription,
+ title: IssuesListApp.i18n.noOpenIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+
+ describe('when "Closed" tab is active', () => {
+ beforeEach(() => {
+ mountComponent({ hasSearch: false, isOpenTab: false });
+ });
+
+ it('shows empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: IssuesListApp.i18n.noClosedIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
new file mode 100644
index 00000000000..065139f10f4
--- /dev/null
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -0,0 +1,211 @@
+import { GlEmptyState, GlLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
+import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
+import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import { i18n } from '~/issues/list/constants';
+
+describe('EmptyStateWithoutAnyIssues component', () => {
+ let wrapper;
+
+ const defaultProps = {
+ currentTabCount: 0,
+ exportCsvPathWithQuery: 'export/csv/path',
+ };
+
+ const defaultProvide = {
+ canCreateProjects: false,
+ emptyStateSvgPath: 'empty/state/svg/path',
+ fullPath: 'full/path',
+ isSignedIn: true,
+ jiraIntegrationPath: 'jira/integration/path',
+ newIssuePath: 'new/issue/path',
+ newProjectPath: 'new/project/path',
+ showNewIssueLink: false,
+ signInPath: 'sign/in/path',
+ };
+
+ const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findIssuesHelpPageLink = () =>
+ wrapper.findByRole('link', { name: i18n.noIssuesDescription });
+ const findJiraDocsLink = () =>
+ wrapper.findByRole('link', { name: 'Enable the Jira integration' });
+ const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
+ const findNewIssueLink = () => wrapper.findByRole('link', { name: i18n.newIssueLabel });
+ const findNewProjectLink = () => wrapper.findByRole('link', { name: i18n.newProjectLabel });
+
+ const mountComponent = ({ props = {}, provide = {} } = {}) => {
+ wrapper = mountExtended(EmptyStateWithoutAnyIssues, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ NewIssueDropdown: true,
+ },
+ });
+ };
+
+ describe('when signed in', () => {
+ describe('empty state', () => {
+ it('renders empty state', () => {
+ mountComponent();
+
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: i18n.noIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ });
+ });
+
+ describe('description', () => {
+ it('renders issues docs link', () => {
+ mountComponent();
+
+ expect(findIssuesHelpPageLink().attributes('href')).toBe(
+ EmptyStateWithoutAnyIssues.issuesHelpPagePath,
+ );
+ });
+
+ describe('"create a project first" description', () => {
+ describe('when can create projects', () => {
+ it('renders', () => {
+ mountComponent({ provide: { canCreateProjects: true } });
+
+ expect(findGlEmptyState().text()).toContain(i18n.noGroupIssuesSignedInDescription);
+ });
+ });
+
+ describe('when cannot create projects', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { canCreateProjects: false } });
+
+ expect(findGlEmptyState().text()).not.toContain(
+ i18n.noGroupIssuesSignedInDescription,
+ );
+ });
+ });
+ });
+ });
+
+ describe('actions', () => {
+ describe('"New project" link', () => {
+ describe('when can create projects', () => {
+ it('renders', () => {
+ mountComponent({ provide: { canCreateProjects: true } });
+
+ expect(findNewProjectLink().attributes('href')).toBe(defaultProvide.newProjectPath);
+ });
+ });
+
+ describe('when cannot create projects', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { canCreateProjects: false } });
+
+ expect(findNewProjectLink().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('"New issue" link', () => {
+ describe('when can show new issue link', () => {
+ it('renders', () => {
+ mountComponent({ provide: { showNewIssueLink: true } });
+
+ expect(findNewIssueLink().attributes('href')).toBe(defaultProvide.newIssuePath);
+ });
+ });
+
+ describe('when cannot show new issue link', () => {
+ it('does not render', () => {
+ mountComponent({ provide: { showNewIssueLink: false } });
+
+ expect(findNewIssueLink().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('CSV import/export buttons', () => {
+ describe('when can show csv buttons', () => {
+ it('renders', () => {
+ mountComponent({ props: { showCsvButtons: true } });
+
+ expect(findCsvImportExportButtons().props()).toMatchObject({
+ exportCsvPath: defaultProps.exportCsvPathWithQuery,
+ issuableCount: 0,
+ });
+ });
+ });
+
+ describe('when cannot show csv buttons', () => {
+ it('does not render', () => {
+ mountComponent({ props: { showCsvButtons: false } });
+
+ expect(findCsvImportExportButtons().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('new issue dropdown', () => {
+ describe('when can show new issue dropdown', () => {
+ it('renders', () => {
+ mountComponent({ props: { showNewIssueDropdown: true } });
+
+ expect(findNewIssueDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when cannot show new issue dropdown', () => {
+ it('does not render', () => {
+ mountComponent({ props: { showNewIssueDropdown: false } });
+
+ expect(findNewIssueDropdown().exists()).toBe(false);
+ });
+ });
+ });
+ });
+ });
+
+ describe('Jira section', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('shows Jira integration information', () => {
+ const paragraphs = wrapper.findAll('p');
+ expect(paragraphs.at(1).text()).toContain(i18n.jiraIntegrationTitle);
+ expect(paragraphs.at(2).text()).toMatchInterpolatedText(i18n.jiraIntegrationMessage);
+ expect(paragraphs.at(3).text()).toContain(i18n.jiraIntegrationSecondaryMessage);
+ });
+
+ it('renders Jira integration docs link', () => {
+ expect(findJiraDocsLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
+ });
+ });
+ });
+
+ describe('when signed out', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { isSignedIn: false } });
+ });
+
+ it('renders empty state', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ title: i18n.noIssuesTitle,
+ svgPath: defaultProvide.emptyStateSvgPath,
+ primaryButtonText: i18n.noIssuesSignedOutButtonText,
+ primaryButtonLink: defaultProvide.signInPath,
+ });
+ });
+
+ it('renders issues docs link', () => {
+ expect(findGlLink().attributes('href')).toBe(EmptyStateWithoutAnyIssues.issuesHelpPagePath);
+ expect(findGlLink().text()).toBe(i18n.noIssuesDescription);
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/components/issue_card_statistics_spec.js b/spec/frontend/issues/list/components/issue_card_statistics_spec.js
new file mode 100644
index 00000000000..180d4ab7eb6
--- /dev/null
+++ b/spec/frontend/issues/list/components/issue_card_statistics_spec.js
@@ -0,0 +1,64 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import IssueCardStatistics from '~/issues/list/components/issue_card_statistics.vue';
+import { i18n } from '~/issues/list/constants';
+
+describe('IssueCardStatistics CE component', () => {
+ let wrapper;
+
+ const findMergeRequests = () => wrapper.findByTestId('merge-requests');
+ const findUpvotes = () => wrapper.findByTestId('issuable-upvotes');
+ const findDownvotes = () => wrapper.findByTestId('issuable-downvotes');
+
+ const mountComponent = ({ mergeRequestsCount, upvotes, downvotes } = {}) => {
+ wrapper = shallowMountExtended(IssueCardStatistics, {
+ propsData: {
+ issue: {
+ mergeRequestsCount,
+ upvotes,
+ downvotes,
+ },
+ },
+ });
+ };
+
+ describe('when issue attributes are undefined', () => {
+ it('does not render the attributes', () => {
+ mountComponent();
+
+ expect(findMergeRequests().exists()).toBe(false);
+ expect(findUpvotes().exists()).toBe(false);
+ expect(findDownvotes().exists()).toBe(false);
+ });
+ });
+
+ describe('when issue attributes are defined', () => {
+ beforeEach(() => {
+ mountComponent({ mergeRequestsCount: 1, upvotes: 5, downvotes: 9 });
+ });
+
+ it('renders merge requests', () => {
+ const mergeRequests = findMergeRequests();
+
+ expect(mergeRequests.text()).toBe('1');
+ expect(mergeRequests.attributes('title')).toBe(i18n.relatedMergeRequests);
+ expect(mergeRequests.findComponent(GlIcon).props('name')).toBe('merge-request');
+ });
+
+ it('renders upvotes', () => {
+ const upvotes = findUpvotes();
+
+ expect(upvotes.text()).toBe('5');
+ expect(upvotes.attributes('title')).toBe(i18n.upvotes);
+ expect(upvotes.findComponent(GlIcon).props('name')).toBe('thumb-up');
+ });
+
+ it('renders downvotes', () => {
+ const downvotes = findDownvotes();
+
+ expect(downvotes.text()).toBe('9');
+ expect(downvotes.attributes('title')).toBe(i18n.downvotes);
+ expect(downvotes.findComponent(GlIcon).props('name')).toBe('thumb-down');
+ });
+ });
+});
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 d0c93c896b3..4c5d8ce3cd1 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -21,18 +21,21 @@ import {
setSortPreferenceMutationResponseWithErrors,
urlParams,
} from 'jest/issues/list/mock_data';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
+import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
import {
CREATED_DESC,
RELATIVE_POSITION,
RELATIVE_POSITION_ASC,
+ UPDATED_DESC,
urlSortParams,
} from '~/issues/list/constants';
import eventHub from '~/issues/list/eventhub';
@@ -58,10 +61,11 @@ import {
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import('~/issuable/bulk_update_sidebar');
+import('~/issuable');
import('~/users_select');
jest.mock('@sentry/browser');
@@ -122,10 +126,8 @@ describe('CE IssuesListApp component', () => {
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
- const findGlButton = () => wrapper.findComponent(GlButton);
const findGlButtons = () => wrapper.findAllComponents(GlButton);
const findGlButtonAt = (index) => findGlButtons().at(index);
- const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
@@ -182,7 +184,11 @@ describe('CE IssuesListApp component', () => {
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
- sortOptions: getSortOptions(true, true),
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
initialSortBy: CREATED_DESC,
issuables: getIssuesQueryResponse.data.project.issues.nodes,
tabs: IssuableListTabs,
@@ -395,9 +401,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -435,9 +441,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -486,136 +492,29 @@ describe('CE IssuesListApp component', () => {
describe('empty states', () => {
describe('when there are issues', () => {
- describe('when search returns no results', () => {
- beforeEach(() => {
- setWindowLocation(`?search=no+results`);
-
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noSearchResultsDescription,
- title: IssuesListApp.i18n.noSearchResultsTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- });
- });
-
- describe('when "Open" tab has no issues', () => {
- beforeEach(() => {
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noOpenIssuesDescription,
- title: IssuesListApp.i18n.noOpenIssuesTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- });
+ beforeEach(() => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
});
- describe('when "Closed" tab has no issues', () => {
- beforeEach(() => {
- setWindowLocation(`?state=${IssuableStates.Closed}`);
-
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noClosedIssuesTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
+ it('shows EmptyStateWithAnyIssues empty state', () => {
+ expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
+ hasSearch: false,
+ isOpenTab: true,
});
});
});
describe('when there are no issues', () => {
- describe('when user is logged in', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { hasAnyIssues: false, isSignedIn: true },
- mountFn: mount,
- });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noIssuesSignedInTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- });
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedInDescription,
- );
- });
-
- it('shows "New issue" and import/export buttons', () => {
- expect(findGlButton().text()).toBe(IssuesListApp.i18n.newIssueLabel);
- expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath);
- expect(findCsvImportExportButtons().props()).toMatchObject({
- exportCsvPath: defaultProvide.exportCsvPath,
- issuableCount: 0,
- });
- });
-
- it('shows Jira integration information', () => {
- const paragraphs = wrapper.findAll('p');
- const links = wrapper.findAll('.gl-link');
- expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
- expect(paragraphs.at(2).text()).toContain(
- 'Enable the Jira integration to view your Jira issues in GitLab.',
- );
- expect(paragraphs.at(3).text()).toContain(
- IssuesListApp.i18n.jiraIntegrationSecondaryMessage,
- );
- expect(links.at(1).text()).toBe('Enable the Jira integration');
- expect(links.at(1).attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
- });
- });
-
- describe('when user is logged in and can create projects', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { canCreateProjects: true, hasAnyIssues: false, isSignedIn: true },
- stubs: { GlEmptyState },
- });
- });
-
- it('shows empty state with additional description about creating projects', () => {
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedInDescription,
- );
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noGroupIssuesSignedInDescription,
- );
- });
-
- it('shows "New project" button', () => {
- expect(findGlButton().text()).toBe(IssuesListApp.i18n.newProjectLabel);
- expect(findGlButton().attributes('href')).toBe(defaultProvide.newProjectPath);
- });
+ beforeEach(() => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: false } });
});
- describe('when user is logged out', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { hasAnyIssues: false, isSignedIn: false },
- mountFn: mount,
- });
- });
-
- it('shows empty state', () => {
- expect(findGlEmptyState().props()).toMatchObject({
- title: IssuesListApp.i18n.noIssuesSignedOutTitle,
- svgPath: defaultProvide.emptyStateSvgPath,
- primaryButtonText: IssuesListApp.i18n.noIssuesSignedOutButtonText,
- primaryButtonLink: defaultProvide.signInPath,
- });
- expect(findGlEmptyState().text()).toContain(
- IssuesListApp.i18n.noIssuesSignedOutDescription,
- );
+ it('shows EmptyStateWithoutAnyIssues empty state', () => {
+ expect(wrapper.findComponent(EmptyStateWithoutAnyIssues).props()).toEqual({
+ currentTabCount: 0,
+ exportCsvPathWithQuery: defaultProvide.exportCsvPath,
+ showCsvButtons: true,
+ showNewIssueDropdown: false,
});
});
});
@@ -636,8 +535,8 @@ describe('CE IssuesListApp component', () => {
it('does not render My-Reaction or Confidential tokens', () => {
expect(findIssuableList().props('searchTokens')).not.toMatchObject([
- { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
- { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers: [mockCurrentUser] },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers: [mockCurrentUser] },
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_CONFIDENTIAL },
]);
@@ -685,13 +584,13 @@ describe('CE IssuesListApp component', () => {
});
it('renders all tokens alphabetically', () => {
- const preloadedAuthors = [
+ const preloadedUsers = [
{ ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
- { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
- { type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers },
{ type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_CONTACT },
{ type: TOKEN_TYPE_LABEL },
@@ -699,6 +598,7 @@ describe('CE IssuesListApp component', () => {
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_ORGANIZATION },
{ type: TOKEN_TYPE_RELEASE },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
{ type: TOKEN_TYPE_TYPE },
]);
});
@@ -899,7 +799,11 @@ describe('CE IssuesListApp component', () => {
it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
async (sortKey) => {
- wrapper = mountComponent();
+ // Ensure initial sort key is different so we can trigger an update when emitting a sort key
+ wrapper =
+ sortKey === CREATED_DESC
+ ? mountComponent({ provide: { initialSort: UPDATED_DESC } })
+ : mountComponent();
router.push = jest.fn();
findIssuableList().vm.$emit('sort', sortKey);
@@ -929,9 +833,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
@@ -941,9 +845,9 @@ describe('CE IssuesListApp component', () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
- findIssuableList().vm.$emit('sort', CREATED_DESC);
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
- expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: CREATED_DESC } });
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } });
});
it('captures error when mutation response has errors', async () => {
@@ -952,7 +856,7 @@ describe('CE IssuesListApp component', () => {
.mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock });
- findIssuableList().vm.$emit('sort', CREATED_DESC);
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
await waitForPromises();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
@@ -1016,9 +920,9 @@ describe('CE IssuesListApp component', () => {
});
it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: IssuesListApp.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
});
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 62fcbf7aad0..0690501dee9 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -1,7 +1,7 @@
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
@@ -132,6 +132,8 @@ export const locationSearch = [
'?search=find+issues',
'author_username=homer',
'not[author_username]=marge',
+ 'or[author_username]=burns',
+ 'or[author_username]=smithers',
'assignee_username[]=bart',
'assignee_username[]=lisa',
'assignee_username[]=5',
@@ -184,41 +186,43 @@ export const locationSearchWithSpecialValues = [
export const filteredTokens = [
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'homer', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'burns', operator: OPERATOR_OR } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'smithers', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: '5', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'carl', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lenny', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'season 4', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'issue', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'feature', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_IS_NOT } },
- { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_EPIC, value: { data: '12', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: '1', operator: OPERATOR_IS } },
- { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
@@ -264,6 +268,7 @@ export const apiParams = {
weight: '3',
},
or: {
+ authorUsernames: ['burns', 'smithers'],
assigneeUsernames: ['carl', 'lenny'],
},
};
@@ -283,6 +288,7 @@ export const apiParamsWithSpecialValues = {
export const urlParams = {
author_username: 'homer',
'not[author_username]': 'marge',
+ 'or[author_username]': ['burns', 'smithers'],
'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
'or[assignee_username][]': ['carl', 'lenny'],
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index 3c6332d5728..a281ed1c989 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -69,26 +69,40 @@ describe('isSortKey', () => {
describe('getSortOptions', () => {
describe.each`
- hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
- ${false} | ${false} | ${10} | ${false} | ${false}
- ${true} | ${false} | ${11} | ${true} | ${false}
- ${false} | ${true} | ${11} | ${false} | ${true}
- ${true} | ${true} | ${12} | ${true} | ${true}
+ hasIssuableHealthStatusFeature | hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsHealthStatus | containsWeight | containsBlocking
+ ${false} | ${false} | ${false} | ${10} | ${false} | ${false} | ${false}
+ ${false} | ${false} | ${true} | ${11} | ${false} | ${false} | ${true}
+ ${false} | ${true} | ${false} | ${11} | ${false} | ${true} | ${false}
+ ${false} | ${true} | ${true} | ${12} | ${false} | ${true} | ${true}
+ ${true} | ${false} | ${false} | ${11} | ${true} | ${false} | ${false}
+ ${true} | ${false} | ${true} | ${12} | ${true} | ${false} | ${true}
+ ${true} | ${true} | ${false} | ${12} | ${true} | ${true} | ${false}
+ ${true} | ${true} | ${true} | ${13} | ${true} | ${true} | ${true}
`(
- 'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
+ 'when hasIssuableHealthStatusFeature=$hasIssuableHealthStatusFeature, hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
({
+ hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasBlockedIssuesFeature,
length,
+ containsHealthStatus,
containsWeight,
containsBlocking,
}) => {
- const sortOptions = getSortOptions(hasIssueWeightsFeature, hasBlockedIssuesFeature);
+ const sortOptions = getSortOptions({
+ hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ });
it('returns the correct length of sort options', () => {
expect(sortOptions).toHaveLength(length);
});
+ it(`${containsHealthStatus ? 'contains' : 'does not contain'} health status option`, () => {
+ expect(sortOptions.some((option) => option.title === 'Health')).toBe(containsHealthStatus);
+ });
+
it(`${containsWeight ? 'contains' : 'does not contain'} weight option`, () => {
expect(sortOptions.some((option) => option.title === 'Weight')).toBe(containsWeight);
});
diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
index 4327fac15d4..d3ec6c3bc9d 100644
--- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/issues/related_merge_requests/store/actions';
import * as types from '~/issues/related_merge_requests/store/mutation_types';
@@ -95,8 +95,8 @@ describe('RelatedMergeRequest store actions', () => {
[],
[{ type: 'requestData' }, { type: 'receiveDataError' }],
);
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: expect.stringMatching('Something went wrong'),
});
});
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 3d027e2084c..6cf44e60092 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -5,7 +5,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import '~/behaviors/markdown/render_gfm';
+import { createAlert } from '~/flash';
import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
import DescriptionComponent from '~/issues/show/components/description.vue';
@@ -26,8 +26,10 @@ import {
zoomMeetingUrl,
} from '../mock_data/mock_data';
-jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
jest.mock('~/issues/show/event_hub');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/behaviors/markdown/render_gfm');
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
@@ -270,9 +272,7 @@ describe('Issuable output', () => {
await wrapper.vm.updateIssuable();
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating issue`,
- );
+ expect(createAlert).toHaveBeenCalledWith({ message: `Error updating issue` });
});
it('returns the correct error message for issuableType', async () => {
@@ -282,9 +282,7 @@ describe('Issuable output', () => {
await nextTick();
await wrapper.vm.updateIssuable();
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating merge request`,
- );
+ expect(createAlert).toHaveBeenCalledWith({ message: `Error updating merge request` });
});
it('shows error message from backend if exists', async () => {
@@ -294,9 +292,9 @@ describe('Issuable output', () => {
.mockRejectedValue({ response: { data: { errors: [msg] } } });
await wrapper.vm.updateIssuable();
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `${wrapper.vm.defaultErrorMessage}. ${msg}`,
- );
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `${wrapper.vm.defaultErrorMessage}. ${msg}`,
+ });
});
});
});
@@ -354,9 +352,7 @@ describe('Issuable output', () => {
.reply(() => Promise.reject(new Error('something went wrong')));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
- 'Error updating issue',
- );
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Error updating issue' });
expect(formSpy).toHaveBeenCalledWith();
});
@@ -402,9 +398,9 @@ describe('Issuable output', () => {
wrapper.setProps({ issuableType: 'merge request' });
return wrapper.vm.updateStoreState().then(() => {
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating ${wrapper.vm.issuableType}`,
- );
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Error updating ${wrapper.vm.issuableType}`,
+ });
});
});
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 9d9abce887b..889ff450825 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import '~/behaviors/markdown/render_gfm';
import { GlTooltip, GlModal } from '@gitlab/ui';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -12,7 +11,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Description from '~/issues/show/components/description.vue';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -21,6 +20,7 @@ import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_ite
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import {
projectWorkItemTypesQueryResponse,
createWorkItemFromTaskMutationResponse,
@@ -37,6 +37,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.fn(),
}));
jest.mock('~/task_list');
+jest.mock('~/behaviors/markdown/render_gfm');
const showModal = jest.fn();
const hideModal = jest.fn();
@@ -161,7 +162,6 @@ describe('Description component', () => {
});
it('applies syntax highlighting and math when description changed', async () => {
- const prototypeSpy = jest.spyOn($.prototype, 'renderGFM');
createComponent();
await wrapper.setProps({
@@ -169,7 +169,7 @@ describe('Description component', () => {
});
expect(findGfmContent().exists()).toBe(true);
- expect(prototypeSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
it('sets data-update-url', () => {
@@ -370,7 +370,7 @@ describe('Description component', () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith(
+ expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Something went wrong when creating task. Please try again.',
}),
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index dc2b3c6fc48..7d6ca44e679 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -3,7 +3,7 @@ import { GlButton, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { IssuableStatus, IssueType } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
@@ -171,19 +171,19 @@ describe('HeaderActions component', () => {
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems} | ${findDesktopDropdown}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems, findDropdown }) => {
describe.each`
- description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
- ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
- ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
- ${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
- ${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
- ${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
+ description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
+ ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
+ ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
+ ${'when user can report abuse'} | ${'Report abuse to administrator'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
+ ${'when user cannot report abuse'} | ${'Report abuse to administrator'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
+ ${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({
@@ -284,9 +284,9 @@ describe('HeaderActions component', () => {
});
it('shows a success message and tells the user they are being redirected', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'The issue was successfully promoted to an epic. Redirecting to epic...',
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
});
@@ -309,7 +309,7 @@ describe('HeaderActions component', () => {
});
it('shows an error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: HeaderActions.i18n.promoteErrorMessage,
});
});
diff --git a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
index 4c1638a9147..81c3c30bf8a 100644
--- a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
+++ b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
@@ -40,5 +40,13 @@ describe('Edit Timeline events', () => {
expect(wrapper.emitted()).toEqual(cancelEvent);
});
+
+ it('should emit the delete event', async () => {
+ const deleteEvent = { delete: [[]] };
+
+ await findTimelineEventsForm().vm.$emit('delete');
+
+ expect(wrapper.emitted()).toEqual(deleteEvent);
+ });
});
});
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 458c1c3f858..33a3a6eddfc 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -1,10 +1,11 @@
-import { GlTab } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import merge from 'lodash/merge';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import DescriptionComponent from '~/issues/show/components/description.vue';
import HighlightBar from '~/issues/show/components/incidents/highlight_bar.vue';
-import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue';
+import IncidentTabs, {
+ incidentTabsI18n,
+} from '~/issues/show/components/incidents/incident_tabs.vue';
import INVALID_URL from '~/lib/utils/invalid_url';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
@@ -16,11 +17,24 @@ const mockAlert = {
iid: '1',
};
+const defaultMocks = {
+ $apollo: {
+ queries: {
+ alert: {
+ loading: true,
+ },
+ timelineEvents: {
+ loading: false,
+ },
+ },
+ },
+};
+
describe('Incident Tabs component', () => {
let wrapper;
- const mountComponent = (data = {}, options = {}) => {
- wrapper = shallowMount(
+ const mountComponent = ({ data = {}, options = {}, mount = shallowMountExtended } = {}) => {
+ wrapper = mount(
IncidentTabs,
merge(
{
@@ -29,7 +43,7 @@ describe('Incident Tabs component', () => {
},
stubs: {
DescriptionComponent: true,
- MetricsTab: true,
+ IncidentMetricTab: true,
},
provide: {
fullPath: '',
@@ -37,41 +51,37 @@ describe('Incident Tabs component', () => {
projectId: '',
issuableId: '',
uploadMetricsFeatureAvailable: true,
+ slaFeatureAvailable: true,
+ canUpdate: true,
+ canUpdateTimelineEvent: true,
},
data() {
return { alert: mockAlert, ...data };
},
- mocks: {
- $apollo: {
- queries: {
- alert: {
- loading: true,
- },
- timelineEvents: {
- loading: false,
- },
- },
- },
- },
+ mocks: defaultMocks,
},
options,
),
);
};
- const findTabs = () => wrapper.findAllComponents(GlTab);
- const findSummaryTab = () => findTabs().at(0);
- const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]');
+ const findSummaryTab = () => wrapper.findByTestId('summary-tab');
+ const findTimelineTab = () => wrapper.findByTestId('timeline-tab');
+ const findAlertDetailsTab = () => wrapper.findByTestId('alert-details-tab');
const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
+ const findTabButtonByFilter = (filter) => wrapper.findAllByRole('tab').filter(filter);
+ const findTimelineTabButton = () =>
+ findTabButtonByFilter((inner) => inner.text() === incidentTabsI18n.timelineTitle).at(0);
+ const findActiveTabs = () => findTabButtonByFilter((inner) => inner.classes('active'));
- describe('empty state', () => {
+ describe('with no alerts', () => {
beforeEach(() => {
- mountComponent({ alert: null });
+ mountComponent({ data: { alert: null } });
});
- it('does not show the alert details tab', () => {
+ it('does not show the alert details tab option', () => {
expect(findAlertDetailsComponent().exists()).toBe(false);
});
});
@@ -83,7 +93,12 @@ describe('Incident Tabs component', () => {
it('renders the summary tab', () => {
expect(findSummaryTab().exists()).toBe(true);
- expect(findSummaryTab().attributes('title')).toBe('Summary');
+ expect(findSummaryTab().attributes('title')).toBe(incidentTabsI18n.summaryTitle);
+ });
+
+ it('renders the timeline tab', () => {
+ expect(findTimelineTab().exists()).toBe(true);
+ expect(findTimelineTab().attributes('title')).toBe(incidentTabsI18n.timelineTitle);
});
it('renders the alert details tab', () => {
@@ -125,4 +140,22 @@ describe('Incident Tabs component', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
+
+ describe('tab changing', () => {
+ beforeEach(() => {
+ mountComponent({ mount: mountExtended });
+ });
+
+ it('shows only the summary tab by default', async () => {
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.summaryTitle);
+ });
+
+ it("shows the timeline tab after it's clicked", async () => {
+ await findTimelineTabButton().trigger('click');
+
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index adea2b6df59..9accfcea791 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -13,6 +13,9 @@ export const mockEvents = [
noteHtml: '<p>Dummy event 1</p>',
occurredAt: '2022-03-22T15:59:00Z',
updatedAt: '2022-03-22T15:59:08Z',
+ timelineEventTags: {
+ nodes: [],
+ },
__typename: 'TimelineEventType',
},
{
@@ -29,6 +32,18 @@ export const mockEvents = [
noteHtml: '<p>Dummy event 2</p>',
occurredAt: '2022-03-23T14:57:00Z',
updatedAt: '2022-03-23T14:57:08Z',
+ timelineEventTags: {
+ nodes: [
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'Start time',
+ },
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'End time',
+ },
+ ],
+ },
__typename: 'TimelineEventType',
},
{
@@ -45,6 +60,9 @@ export const mockEvents = [
noteHtml: '<p>Dummy event 3</p>',
occurredAt: '2022-03-23T15:59:00Z',
updatedAt: '2022-03-23T15:59:08Z',
+ timelineEventTags: {
+ nodes: [],
+ },
__typename: 'TimelineEventType',
},
];
@@ -152,6 +170,9 @@ export const mockGetTimelineData = {
action: 'comment',
occurredAt: '2022-07-01T12:47:00Z',
createdAt: '2022-07-20T12:47:40Z',
+ timelineEventTags: {
+ nodes: [],
+ },
},
],
},
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index 0ce3f75f576..d5b199cc790 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -22,11 +22,12 @@ describe('Timeline events form', () => {
useFakeDate(fakeDate);
let wrapper;
- const mountComponent = ({ mountMethod = shallowMountExtended } = {}) => {
+ const mountComponent = ({ mountMethod = shallowMountExtended } = {}, props = {}) => {
wrapper = mountMethod(TimelineEventsForm, {
propsData: {
showSaveAndAdd: true,
isEventProcessed: false,
+ ...props,
},
stubs: {
GlButton: true,
@@ -43,6 +44,7 @@ describe('Timeline events form', () => {
const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save);
const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd);
const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel);
+ const findDeleteButton = () => wrapper.findByText(timelineFormI18n.delete);
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
@@ -68,6 +70,9 @@ describe('Timeline events form', () => {
findCancelButton().vm.$emit('click');
await waitForPromises();
};
+ const deleteForm = () => {
+ findDeleteButton().vm.$emit('click');
+ };
it('renders markdown-field component with correct list of toolbar items', () => {
mountComponent({ mountMethod: mountExtended });
@@ -165,4 +170,38 @@ describe('Timeline events form', () => {
expect(findSubmitAndAddButton().props('disabled')).toBe(true);
});
});
+
+ describe('Delete button', () => {
+ it('does not show the delete button if showDelete prop is false', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: false });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('shows the delete button if showDelete prop is true', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true });
+
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('disables the delete button if isEventProcessed prop is true', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+
+ expect(findDeleteButton().props('disabled')).toBe(true);
+ });
+
+ it('does not disable the delete button if isEventProcessed prop is false', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: false });
+
+ expect(findDeleteButton().props('disabled')).toBe(false);
+ });
+
+ it('emits delete event on click', () => {
+ mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+
+ deleteForm();
+
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
index 1bf8d68efd4..ba0527e5395 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
@@ -1,5 +1,5 @@
import timezoneMock from 'timezone-mock';
-import { GlIcon, GlDropdown } from '@gitlab/ui';
+import { GlIcon, GlDropdown, GlBadge } from '@gitlab/ui';
import { nextTick } from 'vue';
import { timelineItemI18n } from '~/issues/show/components/incidents/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -27,25 +27,24 @@ describe('IncidentTimelineEventList', () => {
const findCommentIcon = () => wrapper.findComponent(GlIcon);
const findEventTime = () => wrapper.findByTestId('event-time');
+ const findEventTag = () => wrapper.findComponent(GlBadge);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
describe('template', () => {
- it('shows comment icon', () => {
+ beforeEach(() => {
mountComponent();
+ });
+ it('shows comment icon', () => {
expect(findCommentIcon().exists()).toBe(true);
});
it('sets correct props for icon', () => {
- mountComponent();
-
expect(findCommentIcon().props('name')).toBe(mockEvents[0].action);
});
it('displays the correct time', () => {
- mountComponent();
-
expect(findEventTime().text()).toBe('15:59 UTC');
});
@@ -58,8 +57,6 @@ describe('IncidentTimelineEventList', () => {
describe(timezone, () => {
beforeEach(() => {
timezoneMock.register(timezone);
-
- mountComponent();
});
afterEach(() => {
@@ -72,10 +69,20 @@ describe('IncidentTimelineEventList', () => {
});
});
+ describe('timeline event tag', () => {
+ it('does not show when tag is not provided', () => {
+ expect(findEventTag().exists()).toBe(false);
+ });
+
+ it('shows when tag is provided', () => {
+ mountComponent({ propsData: { eventTag: 'Start time' } });
+
+ expect(findEventTag().exists()).toBe(true);
+ });
+ });
+
describe('action dropdown', () => {
it('does not show the action dropdown by default', () => {
- mountComponent();
-
expect(findDropdown().exists()).toBe(false);
expect(findDeleteButton().exists()).toBe(false);
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
index dff1c429d07..a7250e8ad0d 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
@@ -92,6 +92,9 @@ describe('IncidentTimelineEventList', () => {
expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt);
expect(findItems().at(1).props('action')).toBe(mockEvents[1].action);
expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml);
+ expect(findItems().at(1).props('eventTag')).toBe(
+ mockEvents[1].timelineEventTags.nodes[0].name,
+ );
});
it('formats dates correctly', () => {
@@ -120,6 +123,20 @@ describe('IncidentTimelineEventList', () => {
});
});
+ describe('getFirstTag', () => {
+ it('returns undefined, when timelineEventTags contains an empty array', () => {
+ const returnedTag = wrapper.vm.getFirstTag(mockEvents[0].timelineEventTags);
+
+ expect(returnedTag).toEqual(undefined);
+ });
+
+ it('returns the first string, when timelineEventTags contains array with at least one tag', () => {
+ const returnedTag = wrapper.vm.getFirstTag(mockEvents[1].timelineEventTags);
+
+ expect(returnedTag).toBe(mockEvents[1].timelineEventTags.nodes[0].name);
+ });
+ });
+
describe('delete functionality', () => {
beforeEach(() => {
mockConfirmAction({ confirmed: true });
diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js
new file mode 100644
index 00000000000..08f0338d41b
--- /dev/null
+++ b/spec/frontend/issues/show/components/locked_warning_spec.js
@@ -0,0 +1,55 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import { IssuableType } from '~/issues/constants';
+import LockedWarning, { i18n } from '~/issues/show/components/locked_warning.vue';
+
+describe('LockedWarning component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(LockedWarning, {
+ propsData: props,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ describe.each([IssuableType.Issue, IssuableType.Epic])(
+ 'with issuableType set to %s',
+ (issuableType) => {
+ let alert;
+ let link;
+ beforeEach(() => {
+ createComponent({ issuableType });
+ alert = findAlert();
+ link = findLink();
+ });
+
+ afterEach(() => {
+ alert = null;
+ link = null;
+ });
+
+ it('displays a non-closable alert', () => {
+ expect(alert.exists()).toBe(true);
+ expect(alert.props('dismissible')).toBe(false);
+ });
+
+ it(`displays correct message`, async () => {
+ expect(alert.text()).toMatchInterpolatedText(sprintf(i18n.alertMessage, { issuableType }));
+ });
+
+ it(`displays a link with correct text`, async () => {
+ expect(link.exists()).toBe(true);
+ expect(link.text()).toBe(`the ${issuableType}`);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index 56eb6d75def..56e425fa4eb 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -29,19 +29,12 @@ const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('SourceBranchDropdown', () => {
let wrapper;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdownItemByText = (text) =>
- findAllDropdownItems().wrappers.find((item) => item.text() === text);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
-
- const assertDropdownItems = () => {
- const dropdownItems = findAllDropdownItems();
- expect(dropdownItems.wrappers).toHaveLength(mockProject.repository.branchNames.length);
- expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
- mockProject.repository.branchNames,
- );
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ const assertListboxItems = () => {
+ const listboxItems = findListbox().props('items');
+ expect(listboxItems).toHaveLength(mockProject.repository.branchNames.length);
+ expect(listboxItems.map((item) => item.text)).toEqual(mockProject.repository.branchNames);
};
function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
@@ -70,8 +63,8 @@ describe('SourceBranchDropdown', () => {
createComponent();
});
- it('sets dropdown `disabled` prop to `true`', () => {
- expect(findDropdown().props('disabled')).toBe(true);
+ it('sets listbox `disabled` prop to `true`', () => {
+ expect(findListbox().props('disabled')).toBe(true);
});
describe('when `selectedProject` becomes specified', () => {
@@ -83,29 +76,30 @@ describe('SourceBranchDropdown', () => {
await waitForPromises();
});
- it('sets dropdown props correctly', () => {
- expect(findDropdown().props()).toMatchObject({
- loading: false,
+ it('sets listbox props correctly', () => {
+ expect(findListbox().props()).toMatchObject({
disabled: false,
- text: 'Select a branch',
+ loading: false,
+ searchable: true,
+ searching: false,
+ toggleText: 'Select a branch',
});
});
- it('renders available source branches as dropdown items', () => {
- assertDropdownItems();
+ it('renders available source branches as listbox items', () => {
+ assertListboxItems();
});
});
});
describe('when `selectedProject` prop is specified', () => {
describe('when branches are loading', () => {
- it('renders loading icon in dropdown', () => {
+ it('sets loading prop to true', () => {
createComponent({
mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
props: { selectedProject: mockSelectedProject },
});
-
- expect(findLoadingIcon().isVisible()).toBe(true);
+ expect(findListbox().props('loading')).toEqual(true);
});
});
@@ -117,7 +111,7 @@ describe('SourceBranchDropdown', () => {
jest.clearAllMocks();
const mockSearchTerm = 'mai';
- await findSearchBox().vm.$emit('input', mockSearchTerm);
+ await findListbox().vm.$emit('search', mockSearchTerm);
expect(mockGetProjectQuery).toHaveBeenCalledWith({
branchNamesLimit: BRANCHES_PER_PAGE,
@@ -134,32 +128,32 @@ describe('SourceBranchDropdown', () => {
await waitForPromises();
});
- it('sets dropdown props correctly', () => {
- expect(findDropdown().props()).toMatchObject({
- loading: false,
+ it('sets listbox props correctly', () => {
+ expect(findListbox().props()).toMatchObject({
disabled: false,
- text: 'Select a branch',
+ loading: false,
+ searchable: true,
+ searching: false,
+ toggleText: 'Select a branch',
});
});
- it('omits monospace styling from dropdown', () => {
- expect(findDropdown().classes()).not.toContain('gl-font-monospace');
+ it('omits monospace styling from listbox', () => {
+ expect(findListbox().classes()).not.toContain('gl-font-monospace');
});
- it('renders available source branches as dropdown items', () => {
- assertDropdownItems();
+ it('renders available source branches as listbox items', () => {
+ assertListboxItems();
});
it("emits `change` event with the repository's `rootRef` by default", () => {
expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]);
});
- describe('when selecting a dropdown item', () => {
+ describe('when selecting a listbox item', () => {
it('emits `change` event with the selected branch name', async () => {
const mockBranchName = mockProject.repository.branchNames[1];
- const itemToSelect = findDropdownItemByText(mockBranchName);
- await itemToSelect.vm.$emit('click');
-
+ findListbox().vm.$emit('select', mockBranchName);
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
});
});
@@ -173,16 +167,12 @@ describe('SourceBranchDropdown', () => {
});
});
- it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
- expect(findDropdownItemByText(mockBranchName).props('isChecked')).toBe(true);
- });
-
- it('sets dropdown text to `selectedBranchName` value', () => {
- expect(findDropdown().props('text')).toBe(mockBranchName);
+ it('sets listbox text to `selectedBranchName` value', () => {
+ expect(findListbox().props('toggleText')).toBe(mockBranchName);
});
- it('adds monospace styling to dropdown', () => {
- expect(findDropdown().classes()).toContain('gl-font-monospace');
+ it('adds monospace styling to listbox', () => {
+ expect(findListbox().classes()).toContain('gl-font-monospace');
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
index 10696d25f17..e98c6ff1054 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
@@ -1,12 +1,14 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue';
import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue';
-import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
+import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
import { updateInstallation } from '~/jira_connect/subscriptions/api';
import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
+import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
jest.mock('~/jira_connect/subscriptions/api', () => {
return {
@@ -21,8 +23,9 @@ describe('SignInGitlabMultiversion', () => {
const mockBasePath = 'gitlab.mycompany.com';
- const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
+ const findSetupInstructions = () => wrapper.findComponent(SetupInstructions);
const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
+ const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
const findSubtitle = () => wrapper.findByTestId('subtitle');
const createComponent = () => {
@@ -59,15 +62,48 @@ describe('SignInGitlabMultiversion', () => {
});
describe('when version is selected', () => {
- beforeEach(() => {
- retrieveBaseUrl.mockReturnValue(mockBasePath);
- createComponent();
+ describe('when on self-managed', () => {
+ beforeEach(() => {
+ retrieveBaseUrl.mockReturnValue(mockBasePath);
+ createComponent();
+ });
+
+ it('renders correct subtitle', () => {
+ expect(findSubtitle().text()).toBe(SignInGitlabMultiversion.i18n.signInSubtitle);
+ });
+
+ it('renders setup instructions', () => {
+ expect(findSetupInstructions().exists()).toBe(true);
+ });
+
+ describe('when SetupInstructions emits `next` event', () => {
+ beforeEach(async () => {
+ findSetupInstructions().vm.$emit('next');
+ await nextTick();
+ });
+
+ it('renders sign in button', () => {
+ expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
+ });
+
+ it('hides setup instructions', () => {
+ expect(findSetupInstructions().exists()).toBe(false);
+ });
+ });
});
- describe('sign in button', () => {
+ describe('when on GitLab.com', () => {
+ beforeEach(() => {
+ retrieveBaseUrl.mockReturnValue(GITLAB_COM_BASE_PATH);
+ createComponent();
+ });
+
+ it('does not render setup instructions', () => {
+ expect(findSetupInstructions().exists()).toBe(false);
+ });
+
it('renders sign in button', () => {
- expect(findSignInOauthButton().exists()).toBe(true);
- expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
+ expect(findSignInOauthButton().props('gitlabBasePath')).toBe(GITLAB_COM_BASE_PATH);
});
describe('when button emits `sign-in` event', () => {
@@ -90,9 +126,5 @@ describe('SignInGitlabMultiversion', () => {
});
});
});
-
- it('renders correct subtitle', () => {
- expect(findSubtitle().text()).toBe(SignInGitlabMultiversion.i18n.signInSubtitle);
- });
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
new file mode 100644
index 00000000000..5496cf008c5
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
@@ -0,0 +1,35 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlLink } from '@gitlab/ui';
+
+import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue';
+
+describe('SetupInstructions', () => {
+ let wrapper;
+
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = () => {
+ wrapper = shallowMount(SetupInstructions);
+ };
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders "Learn more" link to documentation', () => {
+ expect(findGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK);
+ });
+
+ describe('when button is clicked', () => {
+ it('emits "next" event', () => {
+ expect(wrapper.emitted('next')).toBeUndefined();
+ findGlButton().vm.$emit('click');
+
+ expect(wrapper.emitted('next')).toHaveLength(1);
+ });
+ });
+ });
+});
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 a72528ae36b..748e151f31b 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
@@ -87,7 +87,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
>
<div
aria-label="The GitLab user to which the Jira user Jane Doe will be mapped"
- class="dropdown b-dropdown gl-new-dropdown w-100 btn-group"
+ class="dropdown b-dropdown gl-dropdown w-100 btn-group"
>
<!---->
<button
@@ -101,7 +101,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<span
- class="gl-new-dropdown-button-text"
+ class="gl-dropdown-button-text"
>
janedoe
</span>
@@ -123,14 +123,14 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
tabindex="-1"
>
<div
- class="gl-new-dropdown-inner"
+ class="gl-dropdown-inner"
>
<!---->
<!---->
<div
- class="gl-new-dropdown-contents"
+ class="gl-dropdown-contents"
>
<!---->
@@ -165,7 +165,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
</div>
<li
- class="gl-new-dropdown-text text-secondary"
+ class="gl-dropdown-text text-secondary"
role="presentation"
>
<p
@@ -218,7 +218,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
>
<div
aria-label="The GitLab user to which the Jira user Fred Chopin will be mapped"
- class="dropdown b-dropdown gl-new-dropdown w-100 btn-group"
+ class="dropdown b-dropdown gl-dropdown w-100 btn-group"
>
<!---->
<button
@@ -232,7 +232,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<span
- class="gl-new-dropdown-button-text"
+ class="gl-dropdown-button-text"
>
mrgitlab
</span>
@@ -254,14 +254,14 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
tabindex="-1"
>
<div
- class="gl-new-dropdown-inner"
+ class="gl-dropdown-inner"
>
<!---->
<!---->
<div
- class="gl-new-dropdown-contents"
+ class="gl-dropdown-contents"
>
<!---->
@@ -296,7 +296,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
</div>
<li
- class="gl-new-dropdown-text text-secondary"
+ class="gl-dropdown-text text-secondary"
role="presentation"
>
<p
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
index 98bdfc3fcbc..14613775791 100644
--- a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
@@ -1,6 +1,10 @@
import { GlFilteredSearch } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATORS_IS,
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
import { mockFailedSearchToken } from '../../mock_data';
@@ -37,11 +41,11 @@ describe('Jobs filtered search', () => {
createComponent();
expect(findStatusToken()).toMatchObject({
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: 'Status',
+ title: TOKEN_TITLE_STATUS,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
});
@@ -65,7 +69,7 @@ describe('Jobs filtered search', () => {
createComponent({ queryString: { statuses: value } });
expect(findFilteredSearch().props('value')).toEqual([
- { type: 'status', value: { data: value, operator: '=' } },
+ { type: TOKEN_TYPE_STATUS, value: { data: value, operator: '=' } },
]);
});
});
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
index 92ce3925a90..fbe5f6a2e11 100644
--- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -2,6 +2,10 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitl
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import JobStatusToken from '~/jobs/components/filtered_search/tokens/job_status_token.vue';
+import {
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
describe('Job Status Token', () => {
let wrapper;
@@ -13,9 +17,9 @@ describe('Job Status Token', () => {
const defaultProps = {
config: {
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: 'Status',
+ title: TOKEN_TITLE_STATUS,
unique: true,
},
value: {
diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js
index 299b607ad78..c6ab259bf46 100644
--- a/spec/frontend/jobs/components/job/empty_state_spec.js
+++ b/spec/frontend/jobs/components/job/empty_state_spec.js
@@ -1,5 +1,7 @@
-import { mount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import EmptyState from '~/jobs/components/job/empty_state.vue';
+import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
+import { mockFullPath, mockId } from './mock_data';
describe('Empty State', () => {
let wrapper;
@@ -7,26 +9,31 @@ describe('Empty State', () => {
const defaultProps = {
illustrationPath: 'illustrations/pending_job_empty.svg',
illustrationSizeClass: 'svg-430',
+ jobId: mockId,
title: 'This job has not started yet',
playable: false,
+ isRetryable: true,
};
const createWrapper = (props) => {
- wrapper = mount(EmptyState, {
+ wrapper = shallowMountExtended(EmptyState, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ projectPath: mockFullPath,
+ },
});
};
const content = 'This job is in pending state and is waiting to be picked by a runner';
const findEmptyStateImage = () => wrapper.find('img');
- const findTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
- const findContent = () => wrapper.find('[data-testid="job-empty-state-content"]');
- const findAction = () => wrapper.find('[data-testid="job-empty-state-action"]');
- const findManualVarsForm = () => wrapper.find('[data-testid="manual-vars-form"]');
+ const findTitle = () => wrapper.findByTestId('job-empty-state-title');
+ const findContent = () => wrapper.findByTestId('job-empty-state-content');
+ const findAction = () => wrapper.findByTestId('job-empty-state-action');
+ const findManualVarsForm = () => wrapper.findComponent(ManualVariablesForm);
afterEach(() => {
if (wrapper?.destroy) {
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index 822528403cf..98f1979db1b 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -1,14 +1,15 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
-import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
+import { GlLoadingIcon } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import EmptyState from '~/jobs/components/job/empty_state.vue';
import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
import ErasedBlock from '~/jobs/components/job/erased_block.vue';
import JobApp from '~/jobs/components/job/job_app.vue';
+import JobLog from '~/jobs/components/log/log.vue';
+import JobLogTopBar from '~/jobs/components/job/job_log_controllers.vue';
import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
import StuckBlock from '~/jobs/components/job/stuck_block.vue';
import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
@@ -40,7 +41,10 @@ describe('Job App', () => {
};
const createComponent = () => {
- wrapper = mount(JobApp, { propsData: { ...props }, store });
+ wrapper = shallowMountExtended(JobApp, {
+ propsData: { ...props },
+ store,
+ });
};
const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => {
@@ -59,22 +63,16 @@ describe('Job App', () => {
const findLoadingComponent = () => wrapper.findComponent(GlLoadingIcon);
const findSidebar = () => wrapper.findComponent(Sidebar);
- const findJobContent = () => wrapper.find('[data-testid="job-content"');
const findStuckBlockComponent = () => wrapper.findComponent(StuckBlock);
- const findStuckBlockWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"');
- const findStuckBlockNoActiveRunners = () =>
- wrapper.find('[data-testid="job-stuck-no-active-runners"');
const findFailedJobComponent = () => wrapper.findComponent(UnmetPrerequisitesBlock);
const findEnvironmentsBlockComponent = () => wrapper.findComponent(EnvironmentsBlock);
const findErasedBlock = () => wrapper.findComponent(ErasedBlock);
- const findArchivedJob = () => wrapper.find('[data-testid="archived-job"]');
const findEmptyState = () => wrapper.findComponent(EmptyState);
- const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]');
- const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
- 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"]');
+ const findJobLog = () => wrapper.findComponent(JobLog);
+ const findJobLogTopBar = () => wrapper.findComponent(JobLogTopBar);
+
+ const findJobContent = () => wrapper.findByTestId('job-content');
+ const findArchivedJob = () => wrapper.findByTestId('archived-job');
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -116,36 +114,6 @@ describe('Job App', () => {
expect(wrapper.vm.shouldRenderCalloutMessage).toBe(true);
}));
});
-
- describe('triggered job', () => {
- beforeEach(() => {
- const aYearAgo = new Date();
- aYearAgo.setFullYear(aYearAgo.getFullYear() - 1);
-
- return setupAndMount({
- jobData: { started: aYearAgo.toISOString(), started_at: aYearAgo.toISOString() },
- });
- });
-
- it('should render provided job information', () => {
- expect(wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim()).toContain(
- 'passed Job test triggered 1 year ago by Root',
- );
- });
-
- it('should render new issue link', () => {
- expect(findJobNewIssueLink().attributes('href')).toEqual(job.new_issue_path);
- });
- });
-
- describe('created job', () => {
- it('should render created key', () =>
- setupAndMount().then(() => {
- expect(
- wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim(),
- ).toContain('passed Job test created 3 weeks ago by Root');
- }));
- });
});
describe('stuck block', () => {
@@ -169,57 +137,10 @@ describe('Job App', () => {
},
}).then(() => {
expect(findStuckBlockComponent().exists()).toBe(true);
- expect(findStuckBlockNoActiveRunners().exists()).toBe(true);
- }));
- });
-
- describe('when available runners can not run specified tag', () => {
- it('renders tags in stuck block when there are no runners', () =>
- setupAndMount({
- jobData: {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- stuck: true,
- runners: {
- available: false,
- online: false,
- },
- },
- }).then(() => {
- expect(findStuckBlockComponent().text()).toContain(job.tags[0]);
- expect(findStuckBlockWithTags().exists()).toBe(true);
- }));
- });
-
- describe('when runners are offline and build has tags', () => {
- it('renders message about job being stuck because of no runners with the specified tags', () =>
- setupAndMount({
- jobData: {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- stuck: true,
- runners: {
- available: true,
- online: true,
- },
- },
- }).then(() => {
- expect(findStuckBlockComponent().text()).toContain(job.tags[0]);
- expect(findStuckBlockWithTags().exists()).toBe(true);
}));
});
- it('does not renders stuck block when there are no runners', () =>
+ it('does not render stuck block when there are runners', () =>
setupAndMount({
jobData: {
runners: { available: true },
@@ -351,45 +272,13 @@ describe('Job App', () => {
setupAndMount({ jobData: { has_trace: true } }).then(() => {
expect(findEmptyState().exists()).toBe(false);
}));
-
- it('displays remaining time for a delayed job', () => {
- const oneHourInMilliseconds = 3600000;
- jest
- .spyOn(Date, 'now')
- .mockImplementation(
- () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds,
- );
- return setupAndMount({ jobData: delayedJobFixture }).then(() => {
- expect(findEmptyState().exists()).toBe(true);
-
- const title = findJobEmptyStateTitle().text();
-
- expect(title).toEqual('This is a delayed job to run in 01:00:00');
- });
- });
});
describe('sidebar', () => {
- it('has no blank blocks', async () => {
- await setupAndMount({
- jobData: {
- duration: null,
- finished_at: null,
- erased_at: null,
- queued: null,
- runner: null,
- coverage: null,
- tags: [],
- cancel_path: null,
- },
- });
+ it('renders sidebar', async () => {
+ await setupAndMount();
- const blocks = wrapper.findAll('.blocks-container > *').wrappers;
- expect(blocks.length).toBeGreaterThan(0);
-
- blocks.forEach((block) => {
- expect(block.text().trim()).not.toBe('');
- });
+ expect(findSidebar().exists()).toBe(true);
});
});
});
@@ -410,31 +299,15 @@ describe('Job App', () => {
});
});
- describe('job log controls', () => {
- beforeEach(() =>
- setupAndMount({
- jobLogData: {
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- complete: true,
- },
- }),
- );
-
- it('should render scroll buttons', () => {
- expect(findJobLogScrollTop().exists()).toBe(true);
- expect(findJobLogScrollBottom().exists()).toBe(true);
- });
+ describe('job log', () => {
+ beforeEach(() => setupAndMount());
- it('should render link to raw ouput', () => {
- expect(findJobLogController().exists()).toBe(true);
+ it('should render job log header', () => {
+ expect(findJobLogTopBar().exists()).toBe(true);
});
- it('should render link to erase job', () => {
- expect(findJobLogEraseLink().exists()).toBe(true);
+ it('should render job log', () => {
+ expect(findJobLog().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
index 18d5f35bde4..91821a38a78 100644
--- a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
@@ -16,6 +16,7 @@ describe('Job Sidebar Retry Button', () => {
wrapper = shallowMountExtended(JobsSidebarRetryButton, {
propsData: {
href: job.retry_path,
+ isManualJob: false,
modalId: 'modal-id',
...props,
},
diff --git a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js b/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js
deleted file mode 100644
index 184562b2968..00000000000
--- a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import { GlSprintf, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue';
-
-Vue.use(Vuex);
-
-describe('Manual Variables Form', () => {
- let wrapper;
- let store;
-
- const requiredProps = {
- action: {
- path: '/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- };
-
- const createComponent = (props = {}) => {
- store = new Vuex.Store({
- actions: {
- triggerManualJob: jest.fn(),
- },
- });
-
- wrapper = extendedWrapper(
- mount(LegacyManualVariablesForm, {
- propsData: { ...requiredProps, ...props },
- store,
- stubs: {
- GlSprintf,
- },
- }),
- );
- };
-
- const findHelpText = () => wrapper.findComponent(GlSprintf);
- const findHelpLink = () => wrapper.findComponent(GlLink);
-
- const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
- const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
- const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
- const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
- const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
- const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
- const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
- const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
-
- const setCiVariableKey = () => {
- findCiVariableKey().setValue('new key');
- findCiVariableKey().vm.$emit('change');
- nextTick();
- };
-
- const setCiVariableKeyByPosition = (position, value) => {
- findAllCiVariableKeys().at(position).setValue(value);
- findAllCiVariableKeys().at(position).vm.$emit('change');
- nextTick();
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('creates a new variable when user enters a new key value', async () => {
- expect(findAllVariables()).toHaveLength(1);
-
- await setCiVariableKey();
-
- expect(findAllVariables()).toHaveLength(2);
- });
-
- it('does not create extra empty variables', async () => {
- expect(findAllVariables()).toHaveLength(1);
-
- await setCiVariableKey();
-
- expect(findAllVariables()).toHaveLength(2);
-
- await setCiVariableKey();
-
- expect(findAllVariables()).toHaveLength(2);
- });
-
- it('removes the correct variable row', async () => {
- const variableKeyNameOne = 'key-one';
- const variableKeyNameThree = 'key-three';
-
- await setCiVariableKeyByPosition(0, variableKeyNameOne);
-
- await setCiVariableKeyByPosition(1, 'key-two');
-
- await setCiVariableKeyByPosition(2, variableKeyNameThree);
-
- expect(findAllVariables()).toHaveLength(4);
-
- await findAllDeleteVarBtns().at(1).trigger('click');
-
- expect(findAllVariables()).toHaveLength(3);
-
- expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
- expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
- expect(findAllCiVariableKeys().at(2).element.value).toBe('');
- });
-
- it('trigger button is disabled after trigger action', async () => {
- expect(findTriggerBtn().props('disabled')).toBe(false);
-
- await findTriggerBtn().trigger('click');
-
- expect(findTriggerBtn().props('disabled')).toBe(true);
- });
-
- it('delete variable button should only show when there is more than one variable', async () => {
- expect(findDeleteVarBtn().exists()).toBe(false);
-
- await setCiVariableKey();
-
- expect(findDeleteVarBtn().exists()).toBe(true);
- });
-
- it('delete variable button placeholder should only exist when a user cannot remove', async () => {
- expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
- });
-
- it('renders help text with provided link', () => {
- expect(findHelpText().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(
- '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
- );
- });
-
- it('passes variables in correct format', async () => {
- jest.spyOn(store, 'dispatch');
-
- await setCiVariableKey();
-
- await findCiVariableValue().setValue('new value');
-
- await findTriggerBtn().trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
- {
- key: 'new key',
- secret_value: 'new value',
- },
- ]);
- });
-});
diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
deleted file mode 100644
index 95eb10118ee..00000000000
--- a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
-import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
-import createStore from '~/jobs/store';
-import job, { failedJobStatus } from '../../mock_data';
-
-describe('Legacy Sidebar Header', () => {
- let store;
- let wrapper;
-
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findRetryButton = () => wrapper.findComponent(JobRetryButton);
- const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
-
- const createWrapper = (props) => {
- store = createStore();
-
- wrapper = extendedWrapper(
- shallowMount(LegacySidebarHeader, {
- propsData: {
- job,
- ...props,
- },
- store,
- }),
- );
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when job log is erasable', () => {
- const path = '/root/ci-project/-/jobs/1447/erase';
-
- beforeEach(() => {
- createWrapper({
- erasePath: path,
- });
- });
-
- it('renders erase job link', () => {
- expect(findEraseLink().exists()).toBe(true);
- });
-
- it('erase job link has correct path', () => {
- expect(findEraseLink().attributes('href')).toBe(path);
- });
- });
-
- describe('when job log is not erasable', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('does not render erase button', () => {
- expect(findEraseLink().exists()).toBe(false);
- });
- });
-
- describe('when the job is retryable', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('should render the retry button', () => {
- expect(findRetryButton().props('href')).toBe(job.retry_path);
- });
-
- it('should have a different label when the job status is passed', () => {
- expect(findRetryButton().attributes('title')).toBe(
- LegacySidebarHeader.i18n.runAgainJobButtonLabel,
- );
- });
- });
-
- describe('when there is no retry path', () => {
- it('should not render a retry button', async () => {
- const copy = { ...job, retry_path: null };
- createWrapper({ job: copy });
-
- expect(findRetryButton().exists()).toBe(false);
- });
- });
-
- describe('when the job is cancelable', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('should render link to cancel job', () => {
- expect(findCancelButton().props('icon')).toBe('cancel');
- expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
- });
- });
-
- describe('when the job is failed', () => {
- describe('retry button', () => {
- it('should have a different label when the job status is failed', () => {
- createWrapper({ job: { ...job, status: failedJobStatus } });
-
- expect(findRetryButton().attributes('title')).toBe(
- LegacySidebarHeader.i18n.retryJobButtonLabel,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
index 5806f9f75f9..45a1e9dca76 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -1,46 +1,71 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { GRAPHQL_ID_TYPES } from '~/jobs/constants';
+import waitForPromises from 'helpers/wait_for_promises';
import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
-
-Vue.use(Vuex);
+import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
+import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql';
+import {
+ mockFullPath,
+ mockId,
+ mockJobResponse,
+ mockJobWithVariablesResponse,
+ mockJobMutationData,
+} from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const defaultProvide = {
+ projectPath: mockFullPath,
+};
describe('Manual Variables Form', () => {
let wrapper;
- let store;
-
- const requiredProps = {
- action: {
- path: '/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
+ let mockApollo;
+ let getJobQueryResponse;
+
+ const createComponent = ({ options = {}, props = {} } = {}) => {
+ wrapper = mountExtended(ManualVariablesForm, {
+ propsData: {
+ ...props,
+ jobId: mockId,
+ isRetryable: true,
+ },
+ provide: {
+ ...defaultProvide,
+ },
+ ...options,
+ });
};
- const createComponent = (props = {}) => {
- store = new Vuex.Store({
- actions: {
- triggerManualJob: jest.fn(),
- },
+ const createComponentWithApollo = async ({ props = {} } = {}) => {
+ const requestHandlers = [[getJobQuery, getJobQueryResponse]];
+
+ mockApollo = createMockApollo(requestHandlers);
+
+ const options = {
+ localVue,
+ apolloProvider: mockApollo,
+ };
+
+ createComponent({
+ props,
+ options,
});
- wrapper = extendedWrapper(
- mount(ManualVariablesForm, {
- propsData: { ...requiredProps, ...props },
- store,
- stubs: {
- GlSprintf,
- },
- }),
- );
+ return waitForPromises();
};
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
-
- const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
+ const findCancelBtn = () => wrapper.findByTestId('cancel-btn');
+ const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
@@ -62,95 +87,134 @@ describe('Manual Variables Form', () => {
};
beforeEach(() => {
- createComponent();
+ getJobQueryResponse = jest.fn();
});
afterEach(() => {
wrapper.destroy();
});
- it('creates a new variable when user enters a new key value', async () => {
- expect(findAllVariables()).toHaveLength(1);
+ describe('when page renders', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobResponse);
+ await createComponentWithApollo();
+ });
+
+ it('renders help text with provided link', () => {
+ expect(findHelpText().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(
+ '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
+ );
+ });
+
+ it('renders buttons', () => {
+ expect(findCancelBtn().exists()).toBe(true);
+ expect(findRerunBtn().exists()).toBe(true);
+ });
+ });
+
+ describe('when job has variables', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse);
+ await createComponentWithApollo();
+ });
- await setCiVariableKey();
+ it('sets manual job variables', () => {
+ const queryKey = mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].key;
+ const queryValue =
+ mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].value;
- expect(findAllVariables()).toHaveLength(2);
+ expect(findCiVariableKey().element.value).toBe(queryKey);
+ expect(findCiVariableValue().element.value).toBe(queryValue);
+ });
});
- it('does not create extra empty variables', async () => {
- expect(findAllVariables()).toHaveLength(1);
+ describe('when mutation fires', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo();
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData);
+ });
- await setCiVariableKey();
+ it('passes variables in correct format', async () => {
+ await setCiVariableKey();
- expect(findAllVariables()).toHaveLength(2);
+ await findCiVariableValue().setValue('new value');
- await setCiVariableKey();
+ await findRerunBtn().vm.$emit('click');
- expect(findAllVariables()).toHaveLength(2);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: retryJobMutation,
+ variables: {
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, mockId),
+ variables: [
+ {
+ key: 'new key',
+ value: 'new value',
+ },
+ ],
+ },
+ });
+ });
});
- it('removes the correct variable row', async () => {
- const variableKeyNameOne = 'key-one';
- const variableKeyNameThree = 'key-three';
+ describe('updating variables in UI', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobResponse);
+ await createComponentWithApollo();
+ });
- await setCiVariableKeyByPosition(0, variableKeyNameOne);
+ it('creates a new variable when user enters a new key value', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- await setCiVariableKeyByPosition(1, 'key-two');
+ await setCiVariableKey();
- await setCiVariableKeyByPosition(2, variableKeyNameThree);
+ expect(findAllVariables()).toHaveLength(2);
+ });
- expect(findAllVariables()).toHaveLength(4);
+ it('does not create extra empty variables', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- await findAllDeleteVarBtns().at(1).trigger('click');
+ await setCiVariableKey();
- expect(findAllVariables()).toHaveLength(3);
+ expect(findAllVariables()).toHaveLength(2);
- expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
- expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
- expect(findAllCiVariableKeys().at(2).element.value).toBe('');
- });
+ await setCiVariableKey();
- it('trigger button is disabled after trigger action', async () => {
- expect(findTriggerBtn().props('disabled')).toBe(false);
+ expect(findAllVariables()).toHaveLength(2);
+ });
- await findTriggerBtn().trigger('click');
+ it('removes the correct variable row', async () => {
+ const variableKeyNameOne = 'key-one';
+ const variableKeyNameThree = 'key-three';
- expect(findTriggerBtn().props('disabled')).toBe(true);
- });
+ await setCiVariableKeyByPosition(0, variableKeyNameOne);
- it('delete variable button should only show when there is more than one variable', async () => {
- expect(findDeleteVarBtn().exists()).toBe(false);
+ await setCiVariableKeyByPosition(1, 'key-two');
- await setCiVariableKey();
+ await setCiVariableKeyByPosition(2, variableKeyNameThree);
- expect(findDeleteVarBtn().exists()).toBe(true);
- });
+ expect(findAllVariables()).toHaveLength(4);
- it('delete variable button placeholder should only exist when a user cannot remove', async () => {
- expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
- });
+ await findAllDeleteVarBtns().at(1).trigger('click');
- it('renders help text with provided link', () => {
- expect(findHelpText().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(
- '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
- );
- });
+ expect(findAllVariables()).toHaveLength(3);
- it('passes variables in correct format', async () => {
- jest.spyOn(store, 'dispatch');
+ expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
+ expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
+ expect(findAllCiVariableKeys().at(2).element.value).toBe('');
+ });
- await setCiVariableKey();
+ it('delete variable button should only show when there is more than one variable', async () => {
+ expect(findDeleteVarBtn().exists()).toBe(false);
- await findCiVariableValue().setValue('new value');
+ await setCiVariableKey();
- await findTriggerBtn().trigger('click');
+ expect(findDeleteVarBtn().exists()).toBe(true);
+ });
- expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
- {
- key: 'new key',
- secret_value: 'new value',
- },
- ]);
+ it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+ expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js
new file mode 100644
index 00000000000..9596e859475
--- /dev/null
+++ b/spec/frontend/jobs/components/job/mock_data.js
@@ -0,0 +1,76 @@
+export const mockFullPath = 'Commit451/lab-coat';
+export const mockId = 401;
+
+export const mockJobResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/4',
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualJob: true,
+ manualVariables: {
+ nodes: [],
+ __typename: 'CiManualVariableConnection',
+ },
+ name: 'manual_job',
+ retryable: true,
+ status: 'SUCCESS',
+ __typename: 'CiJob',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockJobWithVariablesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/4',
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualJob: true,
+ manualVariables: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::JobVariable/150',
+ key: 'new key',
+ value: 'new value',
+ __typename: 'CiManualVariable',
+ },
+ ],
+ __typename: 'CiManualVariableConnection',
+ },
+ name: 'manual_job',
+ retryable: true,
+ status: 'SUCCESS',
+ __typename: 'CiJob',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockJobMutationData = {
+ data: {
+ jobRetry: {
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualVariables: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::JobVariable/151',
+ key: 'new key',
+ value: 'new value',
+ __typename: 'CiManualVariable',
+ },
+ ],
+ __typename: 'CiManualVariableConnection',
+ },
+ webPath: '/Commit451/lab-coat/-/jobs/401',
+ __typename: 'CiJob',
+ },
+ errors: [],
+ __typename: 'JobRetryPayload',
+ },
+ },
+};
diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js
index cb32ca9d3dc..da97945f9bf 100644
--- a/spec/frontend/jobs/components/job/sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js
@@ -1,91 +1,87 @@
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import Vue from 'vue';
+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 SidebarHeader from '~/jobs/components/job/sidebar/sidebar_header.vue';
import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
-import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
-import createStore from '~/jobs/store';
-import job from '../../mock_data';
+import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
+import { mockFullPath, mockId, mockJobResponse } from './mock_data';
-describe('Legacy Sidebar Header', () => {
- let store;
+Vue.use(VueApollo);
+
+const defaultProvide = {
+ projectPath: mockFullPath,
+};
+
+describe('Sidebar Header', () => {
let wrapper;
- const findCancelButton = () => wrapper.findByTestId('cancel-button');
- const findRetryButton = () => wrapper.findComponent(JobRetryButton);
- const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
-
- const createWrapper = (props) => {
- store = createStore();
-
- wrapper = extendedWrapper(
- shallowMount(LegacySidebarHeader, {
- propsData: {
- job,
- ...props,
- },
- store,
- }),
- );
+ const createComponent = ({ options = {}, props = {}, restJob = {} } = {}) => {
+ wrapper = shallowMountExtended(SidebarHeader, {
+ propsData: {
+ ...props,
+ jobId: mockId,
+ restJob,
+ },
+ provide: {
+ ...defaultProvide,
+ },
+ ...options,
+ });
};
- afterEach(() => {
- wrapper.destroy();
- });
+ const createComponentWithApollo = async ({ props = {}, restJob = {} } = {}) => {
+ const getJobQueryResponse = jest.fn().mockResolvedValue(mockJobResponse);
- describe('when job log is erasable', () => {
- const path = '/root/ci-project/-/jobs/1447/erase';
+ const requestHandlers = [[getJobQuery, getJobQueryResponse]];
- beforeEach(() => {
- createWrapper({
- erasePath: path,
- });
- });
+ const apolloProvider = createMockApollo(requestHandlers);
- it('renders erase job link', () => {
- expect(findEraseLink().exists()).toBe(true);
- });
+ const options = {
+ apolloProvider,
+ };
- it('erase job link has correct path', () => {
- expect(findEraseLink().attributes('href')).toBe(path);
+ createComponent({
+ props,
+ restJob,
+ options,
});
- });
- describe('when job log is not erasable', () => {
- beforeEach(() => {
- createWrapper();
- });
+ return waitForPromises();
+ };
- it('does not render erase button', () => {
- expect(findEraseLink().exists()).toBe(false);
- });
- });
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findEraseButton = () => wrapper.findByTestId('job-log-erase-link');
+ const findJobName = () => wrapper.findByTestId('job-name');
+ const findRetryButton = () => wrapper.findComponent(JobRetryButton);
- describe('when the job is retryable', () => {
- beforeEach(() => {
- createWrapper();
+ describe('when rendering contents', () => {
+ it('renders the correct job name', async () => {
+ await createComponentWithApollo();
+ expect(findJobName().text()).toBe(mockJobResponse.data.project.job.name);
});
- it('should render the retry button', () => {
- expect(findRetryButton().props('href')).toBe(job.retry_path);
+ it('does not render buttons with no paths', async () => {
+ await createComponentWithApollo();
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findEraseButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(false);
});
- });
-
- describe('when there is no retry path', () => {
- it('should not render a retry button', async () => {
- const copy = { ...job, retry_path: null };
- createWrapper({ job: copy });
- expect(findRetryButton().exists()).toBe(false);
+ it('renders a retry button with a path', async () => {
+ await createComponentWithApollo({ restJob: { retry_path: 'retry/path' } });
+ expect(findRetryButton().exists()).toBe(true);
});
- });
- describe('when the job is cancelable', () => {
- beforeEach(() => {
- createWrapper();
+ it('renders a cancel button with a path', async () => {
+ await createComponentWithApollo({ restJob: { cancel_path: 'cancel/path' } });
+ expect(findCancelButton().exists()).toBe(true);
});
- it('should render link to cancel job', () => {
- expect(findCancelButton().props('icon')).toBe('cancel');
- expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
+ it('renders an erase button with a path', async () => {
+ await createComponentWithApollo({ restJob: { erase_path: 'erase/path' } });
+ expect(findEraseButton().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index a7fe6d5a626..9abd610c26d 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -3,6 +3,7 @@ import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json';
import { TEST_HOST } from 'spec/test_constants';
+import { TOKEN_TYPE_STATUS } from '~/vue_shared/components/filtered_search_bar/constants';
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
@@ -1365,7 +1366,10 @@ export const CIJobConnectionExistingCache = {
statuses: 'PENDING',
};
-export const mockFailedSearchToken = { type: 'status', value: { data: 'FAILED', operator: '=' } };
+export const mockFailedSearchToken = {
+ type: TOKEN_TYPE_STATUS,
+ value: { data: 'FAILED', operator: '=' },
+};
export const retryMutationResponse = {
data: {
diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js
new file mode 100644
index 00000000000..6a1b94cd813
--- /dev/null
+++ b/spec/frontend/language_switcher/components/app_spec.js
@@ -0,0 +1,62 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import LanguageSwitcherApp from '~/language_switcher/components/app.vue';
+import { PREFERRED_LANGUAGE_COOKIE_KEY } from '~/language_switcher/constants';
+import * as utils from '~/lib/utils/common_utils';
+import { locales, ES, EN } from '../mock_data';
+
+jest.mock('~/lib/utils/common_utils');
+
+describe('<LanguageSwitcher />', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(LanguageSwitcherApp, {
+ provide: {
+ locales,
+ preferredLocale: EN,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getPreferredLanguage = () => wrapper.find('.gl-dropdown-button-text').text();
+ const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`);
+
+ it('preferred language', () => {
+ expect(getPreferredLanguage()).toBe(EN.text);
+
+ createComponent({
+ preferredLocale: ES,
+ });
+
+ expect(getPreferredLanguage()).toBe(ES.text);
+ });
+
+ it('switches language', async () => {
+ // because window.location is **READ ONLY** we cannot simply use
+ // jest.spyOn to mock it.
+ const originalLocation = window.location;
+ delete window.location;
+ window.location = {};
+ window.location.reload = jest.fn();
+ const reloadSpy = window.location.reload;
+ expect(reloadSpy).not.toHaveBeenCalled();
+ expect(utils.setCookie).not.toHaveBeenCalled();
+
+ const es = findLanguageDropdownItem(ES.value);
+
+ await es.trigger('click');
+
+ expect(reloadSpy).toHaveBeenCalled();
+ expect(utils.setCookie).toHaveBeenCalledWith(PREFERRED_LANGUAGE_COOKIE_KEY, ES.value);
+ window.location = originalLocation;
+ });
+});
diff --git a/spec/frontend/language_switcher/mock_data.js b/spec/frontend/language_switcher/mock_data.js
new file mode 100644
index 00000000000..548bddf0173
--- /dev/null
+++ b/spec/frontend/language_switcher/mock_data.js
@@ -0,0 +1,26 @@
+export const EN = {
+ value: 'en',
+ text: 'English',
+};
+
+export const ZH_CN = {
+ value: 'zh_CN',
+ text: '简体中文',
+};
+
+export const ES = {
+ value: 'es',
+ text: 'Espanol',
+};
+
+export const ZH_HK = {
+ value: 'zh_HK',
+ text: '繁体中文(香港)',
+};
+
+export const ZH_TW = {
+ value: 'zh_TW',
+ text: '繁体中文(台湾)',
+};
+
+export const locales = [EN, ZH_CN, ES, ZH_HK, ZH_TW];
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index 412408ce377..f767a673553 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -94,6 +94,11 @@ describe('~/lib/dompurify', () => {
expect(sanitize('<link rel="stylesheet" href="styles.css">')).toBe('');
});
+ it("doesn't allow form tags", () => {
+ expect(sanitize('<form>')).toBe('');
+ expect(sanitize('<form method="post" action="path"></form>')).toBe('');
+ });
+
describe.each`
type | gon
${'root'} | ${rootGon}
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 947c38c8ae8..08ba78cddff 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1,4 +1,5 @@
import * as commonUtils from '~/lib/utils/common_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
describe('common_utils', () => {
describe('getPagePath', () => {
@@ -1069,4 +1070,35 @@ describe('common_utils', () => {
expect(result).toEqual([{ hello: '' }, { helloWorld: '' }]);
});
});
+
+ describe('useNewFonts', () => {
+ let beforeGon;
+ const beforeLocation = window.location.href;
+
+ beforeEach(() => {
+ window.gon = window.gon || {};
+ beforeGon = { ...window.gon };
+ });
+
+ describe.each`
+ featureFlag | queryParameter | fontEnabled
+ ${false} | ${false} | ${false}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ `('new font', ({ featureFlag, queryParameter, fontEnabled }) => {
+ it(`will ${fontEnabled ? '' : 'NOT '}be applied when feature flag is ${
+ featureFlag ? '' : 'NOT '
+ }set and query parameter is ${queryParameter ? '' : 'NOT '}present`, () => {
+ const search = queryParameter ? `?new_fonts` : '';
+ setWindowLocation(search);
+ window.gon = { features: { newFonts: featureFlag } };
+ expect(commonUtils.useNewFonts()).toBe(fontEnabled);
+ });
+ });
+
+ afterEach(() => {
+ window.gon = beforeGon;
+ setWindowLocation(beforeLocation);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/create_and_submit_form_spec.js b/spec/frontend/lib/utils/create_and_submit_form_spec.js
new file mode 100644
index 00000000000..9f2472c60f7
--- /dev/null
+++ b/spec/frontend/lib/utils/create_and_submit_form_spec.js
@@ -0,0 +1,73 @@
+import csrf from '~/lib/utils/csrf';
+import { TEST_HOST } from 'helpers/test_constants';
+import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+const TEST_URL = '/foo/bar/lorem';
+const TEST_DATA = {
+ 'test_thing[0]': 'Lorem Ipsum',
+ 'test_thing[1]': 'Dolar Sit',
+ x: 123,
+};
+const TEST_CSRF = 'testcsrf00==';
+
+describe('~/lib/utils/create_and_submit_form', () => {
+ let submitSpy;
+
+ const findForm = () => document.querySelector('form');
+ const findInputsModel = () =>
+ Array.from(findForm().querySelectorAll('input')).map((inputEl) => ({
+ type: inputEl.type,
+ name: inputEl.name,
+ value: inputEl.value,
+ }));
+
+ beforeEach(() => {
+ submitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit');
+ document.head.innerHTML = `<meta name="csrf-token" content="${TEST_CSRF}">`;
+ csrf.init();
+ });
+
+ afterEach(() => {
+ document.head.innerHTML = '';
+ document.body.innerHTML = '';
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createAndSubmitForm({
+ url: TEST_URL,
+ data: TEST_DATA,
+ });
+ });
+
+ it('creates form', () => {
+ const form = findForm();
+
+ expect(form.action).toBe(joinPaths(TEST_HOST, TEST_URL));
+ expect(form.method).toBe('post');
+ expect(form.style).toMatchObject({
+ display: 'none',
+ });
+ });
+
+ it('creates inputs', () => {
+ expect(findInputsModel()).toEqual([
+ ...Object.keys(TEST_DATA).map((key) => ({
+ type: 'hidden',
+ name: key,
+ value: String(TEST_DATA[key]),
+ })),
+ {
+ type: 'hidden',
+ name: 'authenticity_token',
+ value: TEST_CSRF,
+ },
+ ]);
+ });
+
+ it('submits form', () => {
+ expect(submitSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index d6bac935970..172f8972653 100644
--- a/spec/frontend/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -10,6 +10,7 @@ import {
getParents,
getParentByTagName,
setAttributes,
+ replaceCommentsWith,
} from '~/lib/utils/dom_utils';
const TEST_MARGIN = 5;
@@ -263,4 +264,21 @@ describe('DOM Utils', () => {
expect(getContentWrapperHeight('.does-not-exist')).toBe('');
});
});
+
+ describe('replaceCommentsWith', () => {
+ let div;
+ beforeEach(() => {
+ div = document.createElement('div');
+ });
+
+ it('replaces the comments in a DOM node with an element', () => {
+ div.innerHTML = '<h1> hi there <!-- some comment --> <p> <!-- another comment -->';
+
+ replaceCommentsWith(div, 'comment');
+
+ expect(div.innerHTML).toBe(
+ '<h1> hi there <comment> some comment </comment> <p> <comment> another comment </comment></p></h1>',
+ );
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/poll_until_complete_spec.js b/spec/frontend/lib/utils/poll_until_complete_spec.js
index 7509f954a84..3ce17ecfc8c 100644
--- a/spec/frontend/lib/utils/poll_until_complete_spec.js
+++ b/spec/frontend/lib/utils/poll_until_complete_spec.js
@@ -1,7 +1,7 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import pollUntilComplete from '~/lib/utils/poll_until_complete';
const endpoint = `${TEST_HOST}/foo`;
@@ -37,7 +37,7 @@ describe('pollUntilComplete', () => {
beforeEach(() => {
mock
.onGet(endpoint)
- .replyOnce(httpStatusCodes.NO_CONTENT, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
.replyOnce(httpStatusCodes.OK, mockData);
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 2c6b603197d..6afdab455a6 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -759,6 +759,19 @@ describe('URL utility', () => {
});
});
+ describe('cleanEndingSeparator', () => {
+ it.each`
+ path | expected
+ ${'foo/bar'} | ${'foo/bar'}
+ ${'/foo/bar/'} | ${'/foo/bar'}
+ ${'foo/bar//'} | ${'foo/bar'}
+ ${'foo/bar/./'} | ${'foo/bar/.'}
+ ${''} | ${''}
+ `('$path becomes $expected', ({ path, expected }) => {
+ expect(urlUtils.cleanEndingSeparator(path)).toBe(expected);
+ });
+ });
+
describe('joinPaths', () => {
it.each`
paths | expected
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index fd41531796b..0816152f4e3 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { getAllByRole, getByTestId } from '@testing-library/dom';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import { initListbox, parseAttributes } from '~/listbox';
import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@@ -41,7 +41,7 @@ describe('initListbox', () => {
describe('given a valid element', () => {
let onChangeSpy;
- const listbox = () => createWrapper(instance).findComponent(GlListbox);
+ const listbox = () => createWrapper(instance).findComponent(GlCollapsibleListbox);
const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index 4580fdb06f2..f346967121c 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -9,6 +9,7 @@ import {
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
} from '~/members/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
jest.mock('~/lib/utils/url_utility', () => {
@@ -130,7 +131,7 @@ describe('MembersFilteredSearchBar', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: {
data: 'foobar',
},
@@ -145,7 +146,7 @@ describe('MembersFilteredSearchBar', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([
{
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: {
data: 'foo bar baz',
},
@@ -174,7 +175,7 @@ describe('MembersFilteredSearchBar', () => {
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
- { type: 'filtered-search-term', value: { data: 'foobar' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
expect(redirectTo).toHaveBeenCalledWith(
@@ -187,7 +188,7 @@ describe('MembersFilteredSearchBar', () => {
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
- { type: 'filtered-search-term', value: { data: 'foo bar baz' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foo bar baz' } },
]);
expect(redirectTo).toHaveBeenCalledWith(
@@ -202,7 +203,7 @@ describe('MembersFilteredSearchBar', () => {
findFilteredSearchBar().vm.$emit('onFilter', [
{ type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
- { type: 'filtered-search-term', value: { data: 'foobar' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
expect(redirectTo).toHaveBeenCalledWith(
@@ -216,7 +217,7 @@ describe('MembersFilteredSearchBar', () => {
createComponent();
findFilteredSearchBar().vm.$emit('onFilter', [
- { type: 'filtered-search-term', value: { data: 'foobar' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
expect(redirectTo).toHaveBeenCalledWith('https://localhost/?search=foobar&tab=invited');
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index c6e90a4b20d..69ff5e47689 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -303,6 +303,7 @@ describe('MergeRequestTabs', () => {
const tabContent = document.createElement('div');
beforeEach(() => {
+ $.fn.renderGFM = jest.fn();
jest.spyOn(mainContent, 'getBoundingClientRect').mockReturnValue({ top: 10 });
jest.spyOn(tabContent, 'getBoundingClientRect').mockReturnValue({ top: 100 });
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
diff --git a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js b/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
new file mode 100644
index 00000000000..3fddbe7ae21
--- /dev/null
+++ b/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
@@ -0,0 +1,80 @@
+import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue';
+
+let wrapper;
+let mock;
+
+function factory() {
+ wrapper = mount(TargetProjectDropdown, {
+ provide: {
+ targetProjectsPath: '/gitlab-org/gitlab/target_projects',
+ currentProject: { value: 1, text: 'gitlab-org/gitlab' },
+ },
+ });
+}
+
+const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+
+describe('Merge requests target project dropdown component', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/gitlab-org/gitlab/target_projects').reply(200, [
+ {
+ id: 10,
+ name: 'Gitlab Test',
+ full_path: '/root/gitlab-test',
+ full_name: 'Administrator / Gitlab Test',
+ refs_url: '/root/gitlab-test/refs',
+ },
+ {
+ id: 1,
+ name: 'Gitlab Test',
+ full_path: '/gitlab-org/gitlab-test',
+ full_name: 'Gitlab Org / Gitlab Test',
+ refs_url: '/gitlab-org/gitlab-test/refs',
+ },
+ ]);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ it('creates hidden input with currentProject ID', () => {
+ factory();
+
+ expect(wrapper.find('[data-testid="target-project-input"]').attributes('value')).toBe('1');
+ });
+
+ it('renders list of projects', async () => {
+ factory();
+
+ wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click');
+
+ await waitForPromises();
+
+ expect(wrapper.findAll('li').length).toBe(2);
+ expect(wrapper.findAll('li').at(0).text()).toBe('root/gitlab-test');
+ expect(wrapper.findAll('li').at(1).text()).toBe('gitlab-org/gitlab-test');
+ });
+
+ it('searches projects', async () => {
+ factory();
+
+ wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click');
+
+ await waitForPromises();
+
+ findDropdown().vm.$emit('search', 'test');
+
+ jest.advanceTimersByTime(500);
+ await waitForPromises();
+
+ expect(mock.history.get[1].params).toEqual({ search: 'test' });
+ });
+});
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index ce5b2a1000b..c20c51db75e 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -346,7 +346,7 @@ describe('Milestone combobox component', () => {
expect(
findFirstProjectMilestonesDropdownItem()
.find('svg')
- .classes('gl-new-dropdown-item-check-icon'),
+ .classes('gl-dropdown-item-check-icon'),
).toBe(true);
selectFirstProjectMilestone();
@@ -473,7 +473,7 @@ describe('Milestone combobox component', () => {
expect(
findFirstGroupMilestonesDropdownItem()
.find('svg')
- .classes('gl-new-dropdown-item-check-icon'),
+ .classes('gl-dropdown-item-check-icon'),
).toBe(true);
selectFirstGroupMilestone();
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
new file mode 100644
index 00000000000..8af0753f929
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
@@ -0,0 +1,233 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MlCandidate renders correctly 1`] = `
+<div>
+ <div
+ class="gl-alert gl-alert-warning"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16 gl-alert-icon"
+ data-testid="warning-icon"
+ role="img"
+ >
+ <use
+ href="#warning"
+ />
+ </svg>
+
+ <div
+ aria-live="assertive"
+ class="gl-alert-content"
+ role="alert"
+ >
+ <h2
+ class="gl-alert-title"
+ >
+ Machine Learning Experiment Tracking is in Incubating Phase
+ </h2>
+
+ <div
+ class="gl-alert-body"
+ >
+
+ GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited
+
+ <a
+ class="gl-link"
+ href="https://about.gitlab.com/handbook/engineering/incubation/"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ Learn more
+ </a>
+ </div>
+
+ <div
+ class="gl-alert-actions"
+ >
+ <a
+ class="btn gl-alert-action btn-confirm btn-md gl-button"
+ href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Feedback
+
+ </span>
+ </a>
+ </div>
+ </div>
+
+ <button
+ aria-label="Dismiss"
+ class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="close-icon"
+ role="img"
+ >
+ <use
+ href="#close"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+
+ <h3>
+
+ Model candidate details
+
+ </h3>
+
+ <table
+ class="candidate-details"
+ >
+ <tbody>
+ <tr
+ class="divider"
+ />
+
+ <tr>
+ <td
+ class="gl-text-secondary gl-font-weight-bold"
+ >
+ Info
+ </td>
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ ID
+ </td>
+
+ <td>
+ candidate_iid
+ </td>
+ </tr>
+
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Status
+ </td>
+
+ <td>
+ SUCCESS
+ </td>
+ </tr>
+
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Experiment
+ </td>
+
+ <td>
+ <a
+ class="gl-link"
+ href="#"
+ >
+ The Experiment
+ </a>
+ </td>
+ </tr>
+
+ <!---->
+
+ <tr
+ class="divider"
+ />
+
+ <tr>
+ <td
+ class="gl-text-secondary gl-font-weight-bold"
+ >
+
+ Parameters
+
+ </td>
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Algorithm
+ </td>
+
+ <td>
+ Decision Tree
+ </td>
+ </tr>
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ MaxDepth
+ </td>
+
+ <td>
+ 3
+ </td>
+ </tr>
+
+ <tr
+ class="divider"
+ />
+
+ <tr>
+ <td
+ class="gl-text-secondary gl-font-weight-bold"
+ >
+
+ Metrics
+
+ </td>
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ AUC
+ </td>
+
+ <td>
+ .55
+ </td>
+ </tr>
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Accuracy
+ </td>
+
+ <td>
+ .99
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+`;
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
index 2eba8869535..e253a0afc6c 100644
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ShowExperiment with candidates renders correctly 1`] = `
+exports[`MlExperiment with candidates renders correctly 1`] = `
<div>
<div
class="gl-alert gl-alert-warning"
@@ -39,7 +39,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn More
+ Learn more
</a>
</div>
@@ -48,7 +48,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
>
<a
class="btn gl-alert-action btn-confirm btn-md gl-button"
- href="https://gitlab.com/groups/gitlab-org/-/epics/8560"
+ href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
>
<!---->
@@ -58,7 +58,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
class="gl-button-text"
>
- Feedback and Updates
+ Feedback
</span>
</a>
@@ -89,13 +89,13 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
<h3>
- Experiment Candidates
+ Experiment candidates
</h3>
<table
aria-busy="false"
- aria-colcount="4"
+ aria-colcount="6"
class="table b-table gl-table gl-mt-0!"
role="table"
>
@@ -150,6 +150,24 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
Mae
</div>
</th>
+ <th
+ aria-colindex="5"
+ aria-label="Details"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div />
+ </th>
+ <th
+ aria-colindex="6"
+ aria-label="Artifact"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div />
+ </th>
</tr>
</thead>
<tbody
@@ -184,6 +202,32 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
/>
+ <td
+ aria-colindex="5"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_candidate1"
+ >
+ Details
+ </a>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_artifact"
+ rel="noopener"
+ target="_blank"
+ >
+ Artifacts
+ </a>
+ </td>
</tr>
<tr
class=""
@@ -213,6 +257,23 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
/>
+ <td
+ aria-colindex="5"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_candidate2"
+ >
+ Details
+ </a>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ />
</tr>
<!---->
<!---->
diff --git a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
index e07a4ed816b..7dca360c7ee 100644
--- a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
@@ -15,7 +15,7 @@ describe('IncubationAlert', () => {
it('displays link to issue', () => {
expect(findButton().attributes().href).toBe(
- 'https://gitlab.com/groups/gitlab-org/-/epics/8560',
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues/381660',
);
});
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
new file mode 100644
index 00000000000..4b16312815a
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
@@ -0,0 +1,43 @@
+import { GlAlert } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
+
+describe('MlCandidate', () => {
+ let wrapper;
+
+ const createWrapper = () => {
+ const candidate = {
+ params: [
+ { name: 'Algorithm', value: 'Decision Tree' },
+ { name: 'MaxDepth', value: '3' },
+ ],
+ metrics: [
+ { name: 'AUC', value: '.55' },
+ { name: 'Accuracy', value: '.99' },
+ ],
+ info: {
+ iid: 'candidate_iid',
+ artifact_link: 'path_to_artifact',
+ experiment_name: 'The Experiment',
+ experiment_path: 'path/to/experiment',
+ status: 'SUCCESS',
+ },
+ };
+
+ return mountExtended(MlCandidate, { provide: { candidate } });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ it('shows incubation warning', () => {
+ wrapper = createWrapper();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('renders correctly', () => {
+ wrapper = createWrapper();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/components/experiment_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
index af722d77532..50539440f25 100644
--- a/spec/frontend/ml/experiment_tracking/components/experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
@@ -1,17 +1,17 @@
import { GlAlert } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
+import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
-describe('ShowExperiment', () => {
+describe('MlExperiment', () => {
let wrapper;
const createWrapper = (candidates = [], metricNames = [], paramNames = []) => {
- return mountExtended(ShowExperiment, { provide: { candidates, metricNames, paramNames } });
+ return mountExtended(MlExperiment, { provide: { candidates, metricNames, paramNames } });
};
const findAlert = () => wrapper.findComponent(GlAlert);
- const findEmptyState = () => wrapper.findByText('This Experiment has no logged Candidates');
+ const findEmptyState = () => wrapper.findByText('This experiment has no logged candidates');
it('shows incubation warning', () => {
wrapper = createWrapper();
@@ -31,8 +31,8 @@ describe('ShowExperiment', () => {
it('renders correctly', () => {
wrapper = createWrapper(
[
- { rmse: 1, l1_ratio: 0.4 },
- { auc: 0.3, l1_ratio: 0.5 },
+ { rmse: 1, l1_ratio: 0.4, details: 'link_to_candidate1', artifact: 'link_to_artifact' },
+ { auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate2' },
],
['rmse', 'auc', 'mae'],
['l1_ratio'],
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 263d6225a9f..3b4554700b4 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -3,7 +3,6 @@
exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="prometheus-graphs"
- data-qa-selector="prometheus_graphs_content"
data-testid="prometheus-graphs"
environmentstate="available"
metricsdashboardbasepath="/monitoring/monitor-project/-/metrics?environment=1"
@@ -40,7 +39,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
>
<dashboards-dropdown-stub
class="flex-grow-1"
- data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
toggle-class="dropdown-menu-toggle"
@@ -60,7 +58,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
class="flex-grow-1"
clearalltext="Clear all"
clearalltextclass="gl-px-5"
- data-qa-selector="environments_dropdown"
data-testid="environments-dropdown"
headertext=""
hideheaderborder="true"
@@ -106,7 +103,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<date-time-picker-stub
class="flex-grow-1 show-last-dropdown"
customenabled="true"
- data-qa-selector="range_picker_dropdown"
options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
value="[object Object]"
/>
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index e00736954a9..cb300870689 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -52,20 +52,6 @@ describe('RefreshButton', () => {
expect(findDropdown().props('text')).toBe('Off');
});
- describe('when feature flag disable_metric_dashboard_refresh_rate is on', () => {
- beforeEach(() => {
- createWrapper({
- provide: {
- glFeatures: { disableMetricDashboardRefreshRate: true },
- },
- });
- });
-
- it('refresh rate is not available', () => {
- expect(findDropdown().exists()).toBe(false);
- });
- });
-
describe('refresh rate options', () => {
it('presents multiple options', () => {
expect(findOptions().length).toBeGreaterThan(1);
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index 6f9af911a9f..def4bfe9443 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -2,7 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, {
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
import { metricsDashboardResponse } from '../fixture_data';
@@ -37,8 +40,8 @@ describe('monitoring metrics_requests', () => {
});
it('returns a dashboard response after retrying twice', () => {
- mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
+ mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
return getDashboard(dashboardEndpoint, params).then((data) => {
@@ -81,8 +84,8 @@ describe('monitoring metrics_requests', () => {
it('returns a dashboard response after retrying twice', () => {
// Mock multiple attempts while the cache is filling up
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response); // 3rd attempt
return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
@@ -116,8 +119,8 @@ describe('monitoring metrics_requests', () => {
it('rejects after retrying twice and getting an HTTP 500 error', () => {
// Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).reply(500, {
status: 'error',
error: 'An error occurred',
@@ -132,7 +135,7 @@ describe('monitoring metrics_requests', () => {
it.each`
code | reason
${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
- ${statusCodes.UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
${statusCodes.SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
`('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
mock.onGet(prometheusEndpoint).reply(code, {
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index ca66768c3cc..93af6526c67 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -4,7 +4,10 @@ import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, {
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import getAnnotations from '~/monitoring/queries/get_annotations.query.graphql';
@@ -944,7 +947,7 @@ describe('Monitoring store actions', () => {
});
it('Succesful POST request resolves', async () => {
- mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
+ mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
dashboard: dashboardGitResponse[1],
});
@@ -969,7 +972,7 @@ describe('Monitoring store actions', () => {
commit_message: 'A new commit message',
});
- mock.onPost(state.dashboardsEndpoint).reply(statusCodes.CREATED, {
+ mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
dashboard: mockCreatedDashboard,
});
@@ -1133,7 +1136,7 @@ describe('Monitoring store actions', () => {
mock
.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(statusCodes.UNPROCESSABLE_ENTITY, {
+ .reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, {
message: mockErrorMsg,
});
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 6c6c3d6b90f..348825c334a 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -435,6 +435,7 @@ describe('monitoring/utils', () => {
describe('setCustomVariablesFromUrl', () => {
beforeEach(() => {
+ window.history.pushState = jest.fn();
jest.spyOn(urlUtils, 'updateHistory');
});
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
new file mode 100644
index 00000000000..f09bdef8caa
--- /dev/null
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -0,0 +1,98 @@
+import { mount, createWrapper } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { getByText as getByTextHelper } from '@testing-library/dom';
+import { GlToggle } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { s__ } from '~/locale';
+
+jest.mock('~/flash');
+
+const TEST_ENDPONT = 'https://example.com/toggle';
+
+describe('NewNavToggle', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+
+ const createComponent = (propsData = { enabled: false }) => {
+ wrapper = mount(NewNavToggle, {
+ propsData: {
+ endpoint: TEST_ENDPONT,
+ ...propsData,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getByText = (text, options) =>
+ createWrapper(getByTextHelper(wrapper.element, text, options));
+
+ it('renders its title', () => {
+ createComponent();
+ expect(getByText('Navigation redesign').exists()).toBe(true);
+ });
+
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: true });
+ });
+
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
+ });
+
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
+
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
+ });
+
+ describe('changing the toggle', () => {
+ useMockLocationHelper();
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent();
+ });
+
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(200);
+ findToggle().vm.$emit('change');
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(500);
+ findToggle().vm.$emit('change');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index a74d709ed3a..add2ed1ba8a 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,6 +1,5 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
-import '~/behaviors/markdown/render_gfm';
import { nextTick } from 'vue';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -11,6 +10,8 @@ import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_sys
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const LINE_RANGE = {};
const DISCUSSION_WITH_LINE_RANGE = {
...discussionMock,
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 2175849aeb9..a90d8bdde06 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -9,7 +9,6 @@ import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_i
import NoteForm from '~/notes/components/note_form.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
import createStore from '~/notes/stores';
-import '~/behaviors/markdown/render_gfm';
import {
noteableDataMock,
discussionMock,
@@ -18,6 +17,8 @@ import {
userDataMock,
} from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('noteable_discussion component', () => {
let store;
let wrapper;
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 9051fcab97f..0c3d0da4f0f 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -2,23 +2,24 @@ import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils';
+import { getLocationHash } from '~/lib/utils/url_utility';
import * as urlUtility from '~/lib/utils/url_utility';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
import NotesActivityHeader from '~/notes/components/notes_activity_header.vue';
import * as constants from '~/notes/constants';
import createStore from '~/notes/stores';
-import '~/behaviors/markdown/render_gfm';
-// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
+// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
import * as mockData from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const TYPE_COMMENT_FORM = 'comment-form';
const TYPE_NOTES_LIST = 'notes-list';
const TEST_NOTES_FILTER_VALUE = 1;
@@ -26,7 +27,6 @@ const TEST_NOTES_FILTER_VALUE = 1;
const propsData = {
noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock,
- userData: mockData.userDataMock,
notesFilters: mockData.notesFilters,
notesFilterValue: TEST_NOTES_FILTER_VALUE,
};
@@ -37,6 +37,19 @@ describe('note_app', () => {
let wrapper;
let store;
+ const initStore = (notesData = propsData.notesData) => {
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setNoteableData', propsData.noteableData);
+ store.dispatch('setUserData', mockData.userDataMock);
+ store.dispatch('setTargetNoteHash', getLocationHash());
+ // call after mounted hook
+ queueMicrotask(() => {
+ queueMicrotask(() => {
+ store.dispatch('fetchNotes');
+ });
+ });
+ };
+
const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
const getComponentOrder = () => {
@@ -51,7 +64,9 @@ describe('note_app', () => {
axiosMock = new AxiosMockAdapter(axios);
store = createStore();
+
mountComponent = ({ props = {} } = {}) => {
+ initStore();
return mount(
{
components: {
@@ -60,6 +75,7 @@ describe('note_app', () => {
template: `<div class="js-vue-notes-event">
<notes-app ref="notesApp" v-bind="$attrs" />
</div>`,
+ inheritAttrs: false,
},
{
propsData: {
@@ -77,53 +93,13 @@ describe('note_app', () => {
axiosMock.restore();
});
- describe('set data', () => {
- beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
- axiosMock.onAny().reply(200, []);
- wrapper = mountComponent();
- return waitForPromises();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('should set notes data', () => {
- expect(store.state.notesData).toEqual(mockData.notesDataMock);
- });
-
- it('should set issue data', () => {
- expect(store.state.noteableData).toEqual(mockData.noteableDataMock);
- });
-
- it('should set user data', () => {
- expect(store.state.userData).toEqual(mockData.userDataMock);
- });
-
- it('should fetch discussions', () => {
- expect(store.state.discussions).toEqual([]);
- });
-
- it('updates discussions badge', () => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('0');
- });
- });
-
describe('render', () => {
beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent();
return waitForPromises();
});
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('should render list of notes', () => {
const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
@@ -148,10 +124,6 @@ describe('note_app', () => {
expect(findCommentButton().props('disabled')).toEqual(true);
});
- it('updates discussions badge', () => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
- });
-
it('should render notes activity header', () => {
expect(wrapper.findComponent(NotesActivityHeader).props()).toEqual({
notesFilterValue: TEST_NOTES_FILTER_VALUE,
@@ -162,8 +134,6 @@ describe('note_app', () => {
describe('render with comments disabled', () => {
beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
wrapper = mountComponent({
// why: In this integration test, previously we manually set store.state.commentsDisabled
@@ -177,10 +147,6 @@ describe('note_app', () => {
return waitForPromises();
});
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('should not render form when commenting is disabled', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
@@ -192,8 +158,6 @@ describe('note_app', () => {
describe('timeline view', () => {
beforeEach(() => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
-
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
store.state.commentsDisabled = false;
store.state.isTimelineEnabled = true;
@@ -202,10 +166,6 @@ describe('note_app', () => {
return waitForPromises();
});
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('should not render comments form', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
@@ -213,14 +173,9 @@ describe('note_app', () => {
describe('while fetching data', () => {
beforeEach(async () => {
- setHTMLFixture('<div class="js-discussions-count"></div>');
wrapper = mountComponent();
});
- afterEach(() => {
- return waitForPromises().then(() => resetHTMLFixture());
- });
-
it('renders skeleton notes', () => {
expect(wrapper.find('.gl-skeleton-loader-default-container').exists()).toBe(true);
});
@@ -231,10 +186,6 @@ describe('note_app', () => {
'Write a comment or drag your files here…',
);
});
-
- it('should not update discussions badge (it should be blank)', () => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('');
- });
});
describe('update note', () => {
@@ -468,7 +419,9 @@ describe('note_app', () => {
describe('fetching discussions', () => {
describe('when note anchor is not present', () => {
it('does not include extra query params', async () => {
- wrapper = shallowMount(NotesApp, { propsData, store: createStore() });
+ store = createStore();
+ initStore();
+ wrapper = shallowMount(NotesApp, { propsData, store });
await waitForPromises();
expect(axiosMock.history.get[0].params).toEqual({ per_page: 20 });
@@ -476,17 +429,16 @@ describe('note_app', () => {
});
describe('when note anchor is present', () => {
- const mountWithNotesFilter = (notesFilter) =>
- shallowMount(NotesApp, {
- propsData: {
- ...propsData,
- notesData: {
- ...propsData.notesData,
- notesFilter,
- },
- },
+ const mountWithNotesFilter = (notesFilter) => {
+ initStore({
+ ...propsData.notesData,
+ notesFilter,
+ });
+ return shallowMount(NotesApp, {
+ propsData,
store: createStore(),
});
+ };
beforeEach(() => {
setWindowLocation('#note_1');
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index d5e2a189afe..f52c3e28691 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -4,7 +4,6 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { createSpyObj } from 'helpers/jest_helpers';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -254,16 +253,20 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
note: 'heya',
html: '<div>heya</div>',
};
- $notesList = createSpyObj('$notesList', ['find', 'append']);
-
- notes = createSpyObj('notes', [
- 'setupNewNote',
- 'refresh',
- 'collapseLongCommitList',
- 'updateNotesCount',
- 'putConflictEditWarningInPlace',
- ]);
- notes.taskList = createSpyObj('tasklist', ['init']);
+ $notesList = {
+ find: jest.fn(),
+ append: jest.fn(),
+ };
+ notes = {
+ setupNewNote: jest.fn(),
+ refresh: jest.fn(),
+ collapseLongCommitList: jest.fn(),
+ updateNotesCount: jest.fn(),
+ putConflictEditWarningInPlace: jest.fn(),
+ };
+ notes.taskList = {
+ init: jest.fn(),
+ };
notes.note_ids = [];
notes.updatedNotesTrackingMap = {};
@@ -383,11 +386,21 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
discussion_resolvable: false,
diff_discussion_html: false,
};
- $form = createSpyObj('$form', ['closest', 'find']);
+ $form = {
+ closest: jest.fn(),
+ find: jest.fn(),
+ };
$form.length = 1;
- row = createSpyObj('row', ['prevAll', 'first', 'find']);
+ row = {
+ prevAll: jest.fn(),
+ first: jest.fn(),
+ find: jest.fn(),
+ };
- notes = createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
+ notes = {
+ isParallelView: jest.fn(),
+ updateNotesCount: jest.fn(),
+ };
notes.note_ids = [];
jest.spyOn(Notes, 'isNewNote');
@@ -403,7 +416,9 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let body;
beforeEach(() => {
- body = createSpyObj('body', ['attr']);
+ body = {
+ attr: jest.fn(),
+ };
discussionContainer = { length: 0 };
$form.closest.mockReturnValueOnce(row).mockReturnValue($form);
@@ -462,7 +477,9 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
noteHTML = '<div></div>';
- $notesList = createSpyObj('$notesList', ['append']);
+ $notesList = {
+ append: jest.fn(),
+ };
$resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
});
@@ -483,7 +500,9 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
noteHTML = '<div></div>';
- $note = createSpyObj('$note', ['replaceWith']);
+ $note = {
+ replaceWith: jest.fn(),
+ };
$updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 989dd74b6d0..dce2e5d370d 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -3,7 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
@@ -13,8 +13,8 @@ import * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import mutations from '~/notes/stores/mutations';
import * as utils from '~/notes/stores/utils';
-import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
-import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_request_lock.mutation.graphql';
import promoteTimelineEvent from '~/notes/graphql/promote_timeline_event.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
import notesEventHub from '~/notes/event_hub';
@@ -30,16 +30,12 @@ import {
} from '../mock_data';
const TEST_ERROR_MESSAGE = 'Test error message';
-const mockFlashClose = jest.fn();
-jest.mock('~/flash', () => {
- const flash = jest.fn().mockImplementation(() => {
- return {
- close: mockFlashClose,
- };
- });
-
- return flash;
-});
+const mockAlertDismiss = jest.fn();
+jest.mock('~/flash', () => ({
+ createAlert: jest.fn().mockImplementation(() => ({
+ dismiss: mockAlertDismiss,
+ })),
+}));
jest.mock('~/vue_shared/plugins/global_toast');
@@ -331,13 +327,13 @@ describe('Actions Notes Store', () => {
await startPolling();
expect(axiosMock.history.get).toHaveLength(1);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
await advanceXMoreIntervals(1);
expect(axiosMock.history.get).toHaveLength(2);
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('resets the failure counter on success', async () => {
@@ -358,14 +354,13 @@ describe('Actions Notes Store', () => {
await advanceXMoreIntervals(1); // Failure #2
// That was the first failure AFTER a success, so we should NOT see the error displayed
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
// Now we'll allow another failure
await advanceXMoreIntervals(1); // Failure #3
// Since this is the second failure in a row, the error should happen
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
});
it('hides the error display if it exists on success', async () => {
@@ -375,16 +370,14 @@ describe('Actions Notes Store', () => {
await advanceXMoreIntervals(2);
// After two errors, the error should be displayed
- expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledTimes(1);
axiosMock.reset();
successMock();
await advanceXMoreIntervals(1);
- expect(mockFlashClose).toHaveBeenCalled();
- expect(mockFlashClose).toHaveBeenCalledTimes(1);
+ expect(mockAlertDismiss).toHaveBeenCalledTimes(1);
});
});
});
@@ -869,7 +862,7 @@ describe('Actions Notes Store', () => {
payload,
),
).rejects.toEqual(error);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -885,8 +878,8 @@ describe('Actions Notes Store', () => {
},
{ ...payload, flashContainer },
);
- expect(resp.hasFlash).toBe(true);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(resp.hasAlert).toBe(true);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Your comment could not be submitted because something went wrong',
parent: flashContainer,
});
@@ -905,7 +898,7 @@ describe('Actions Notes Store', () => {
payload,
);
expect(data).toBe(res);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -943,7 +936,7 @@ describe('Actions Notes Store', () => {
['resolveDiscussion', { discussionId }],
['restartPolling'],
]);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -958,7 +951,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: TEST_ERROR_MESSAGE,
parent: flashContainer,
});
@@ -976,7 +969,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while applying the suggestion. Please try again.',
parent: flashContainer,
});
@@ -987,7 +980,7 @@ describe('Actions Notes Store', () => {
dispatch.mockReturnValue(Promise.reject());
return testSubmitSuggestion(() => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -1029,7 +1022,7 @@ describe('Actions Notes Store', () => {
['restartPolling'],
]);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -1047,7 +1040,7 @@ describe('Actions Notes Store', () => {
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: TEST_ERROR_MESSAGE,
parent: flashContainer,
});
@@ -1068,7 +1061,7 @@ describe('Actions Notes Store', () => {
]);
expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message:
'Something went wrong while applying the batch of suggestions. Please try again.',
parent: flashContainer,
@@ -1088,7 +1081,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
});
});
@@ -1234,7 +1227,7 @@ describe('Actions Notes Store', () => {
),
).rejects.toEqual(new Error());
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -1414,7 +1407,7 @@ describe('Actions Notes Store', () => {
return actions
.promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs)
.then(() => {
- expect(createFlash).toHaveBeenCalledWith(expectedAlertArgs);
+ expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
expect(commitSpy).toHaveBeenCalledWith(
mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS,
false,
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
index f0b318e69ec..248b0a2057c 100644
--- a/spec/frontend/observability/observability_app_spec.js
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -1,5 +1,16 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ObservabilityApp from '~/observability/components/observability_app.vue';
+import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+
+import {
+ MESSAGE_EVENT_TYPE,
+ OBSERVABILITY_ROUTES,
+ SKELETON_VARIANT,
+} from '~/observability/constants';
+
+import { darkModeEnabled } from '~/lib/utils/color_utils';
+
+jest.mock('~/lib/utils/color_utils');
describe('Observability root app', () => {
let wrapper;
@@ -12,6 +23,8 @@ describe('Observability root app', () => {
query: { otherQuery: 100 },
};
+ const mockHandleSkeleton = jest.fn();
+
const findIframe = () => wrapper.findByTestId('observability-ui-iframe');
const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
@@ -21,6 +34,9 @@ describe('Observability root app', () => {
propsData: {
observabilityIframeSrc: TEST_IFRAME_SRC,
},
+ stubs: {
+ 'observability-skeleton': ObservabilitySkeleton,
+ },
mocks: {
$router,
$route: route,
@@ -28,46 +44,156 @@ describe('Observability root app', () => {
});
};
+ const dispatchMessageEvent = (message) =>
+ window.dispatchEvent(new MessageEvent('message', message));
+
afterEach(() => {
wrapper.destroy();
});
- it('should render an iframe with observabilityIframeSrc as src', () => {
- mountComponent();
- const iframe = findIframe();
- expect(iframe.exists()).toBe(true);
- expect(iframe.attributes('src')).toBe(TEST_IFRAME_SRC);
+ describe('iframe src', () => {
+ const TEST_USERNAME = 'test-user';
+
+ beforeAll(() => {
+ gon.current_username = TEST_USERNAME;
+ });
+
+ it('should render an iframe with observabilityIframeSrc, decorated with light theme and username', () => {
+ darkModeEnabled.mockReturnValueOnce(false);
+ mountComponent();
+ const iframe = findIframe();
+
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('src')).toBe(
+ `${TEST_IFRAME_SRC}&theme=light&username=${TEST_USERNAME}`,
+ );
+ });
+
+ it('should render an iframe with observabilityIframeSrc decorated with dark theme and username', () => {
+ darkModeEnabled.mockReturnValueOnce(true);
+ mountComponent();
+ const iframe = findIframe();
+
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('src')).toBe(
+ `${TEST_IFRAME_SRC}&theme=dark&username=${TEST_USERNAME}`,
+ );
+ });
});
- it('should not call replace method from vue router if message event does not have url', () => {
- mountComponent();
- wrapper.vm.messageHandler({ data: 'some other data' });
- expect(replace).not.toHaveBeenCalled();
+ describe('iframe sandbox', () => {
+ it('should render an iframe with sandbox attributes', () => {
+ mountComponent();
+ const iframe = findIframe();
+
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
+ });
});
- it.each`
- condition | origin | observability_path | url
- ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'}
- ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'}
- `(
- 'should not call replace method from vue router if $condition',
- async ({ origin, observability_path, url }) => {
- mountComponent({ ...$route, query: { observability_path } });
- wrapper.vm.messageHandler({ data: { url }, origin });
+ describe('on GOUI_ROUTE_UPDATE', () => {
+ it('should not call replace method from vue router if message event does not have url', () => {
+ mountComponent();
+ dispatchMessageEvent({
+ type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE,
+ payload: { data: 'some other data' },
+ });
expect(replace).not.toHaveBeenCalled();
- },
- );
-
- it('should call replace method from vue router on messageHandle call', () => {
- mountComponent();
- wrapper.vm.messageHandler({ data: { url: '/explore' }, origin: 'https://observe.gitlab.com' });
- expect(replace).toHaveBeenCalled();
- expect(replace).toHaveBeenCalledWith({
- name: 'https://gitlab.com/gitlab-org/',
- query: {
- otherQuery: 100,
- observability_path: '/explore',
+ });
+
+ it.each`
+ condition | origin | observability_path | url
+ ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'}
+ ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'}
+ `(
+ 'should not call replace method from vue router if $condition',
+ async ({ origin, observability_path, url }) => {
+ mountComponent({ ...$route, query: { observability_path } });
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url } },
+ origin,
+ });
+ expect(replace).not.toHaveBeenCalled();
},
+ );
+
+ it('should call replace method from vue router on message event callback', () => {
+ mountComponent();
+
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
+ origin: 'https://observe.gitlab.com',
+ });
+
+ expect(replace).toHaveBeenCalled();
+ expect(replace).toHaveBeenCalledWith({
+ name: 'https://gitlab.com/gitlab-org/',
+ query: {
+ otherQuery: 100,
+ observability_path: '/explore',
+ },
+ });
+ });
+ });
+
+ describe('on GOUI_LOADED', () => {
+ beforeEach(() => {
+ mountComponent();
+ wrapper.vm.$refs.iframeSkeleton.handleSkeleton = mockHandleSkeleton;
+ });
+ it('should call handleSkeleton method', () => {
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
+ origin: 'https://observe.gitlab.com',
+ });
+ expect(mockHandleSkeleton).toHaveBeenCalled();
+ });
+
+ it('should not call handleSkeleton method if origin is different', () => {
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
+ origin: 'https://example.com',
+ });
+ expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ });
+
+ it('should not call handleSkeleton method if event type is different', () => {
+ dispatchMessageEvent({
+ data: { type: 'UNKNOWN_EVENT' },
+ origin: 'https://observe.gitlab.com',
+ });
+ expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('skeleton variant', () => {
+ it.each`
+ pathDescription | path | variant
+ ${'dashboards'} | ${OBSERVABILITY_ROUTES.DASHBOARDS} | ${SKELETON_VARIANT.DASHBOARDS}
+ ${'explore'} | ${OBSERVABILITY_ROUTES.EXPLORE} | ${SKELETON_VARIANT.EXPLORE}
+ ${'manage dashboards'} | ${OBSERVABILITY_ROUTES.MANAGE} | ${SKELETON_VARIANT.MANAGE}
+ ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANT.DASHBOARDS}
+ `('renders the $variant skeleton variant for $pathDescription path', ({ path, variant }) => {
+ mountComponent({ ...$route, path });
+ const props = wrapper.findComponent(ObservabilitySkeleton).props();
+
+ expect(props.variant).toBe(variant);
+ });
+ });
+
+ describe('on observability ui unmount', () => {
+ it('should remove message event and should not call replace method from vue router', () => {
+ mountComponent();
+ wrapper.destroy();
+
+ // testing event cleanup logic, should not call on messege event after component is destroyed
+
+ dispatchMessageEvent({
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
+ origin: 'https://observe.gitlab.com',
+ });
+
+ expect(replace).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
new file mode 100644
index 00000000000..5637c0e6d70
--- /dev/null
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -0,0 +1,96 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+import DashboardsSkeleton from '~/observability/components/skeleton/dashboards.vue';
+import ExploreSkeleton from '~/observability/components/skeleton/explore.vue';
+import ManageSkeleton from '~/observability/components/skeleton/manage.vue';
+
+import { SKELETON_VARIANT } from '~/observability/constants';
+
+describe('ObservabilitySkeleton component', () => {
+ let wrapper;
+
+ const mountComponent = ({ ...props } = {}) => {
+ wrapper = shallowMountExtended(ObservabilitySkeleton, {
+ propsData: props,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('on mount', () => {
+ beforeEach(() => {
+ jest.spyOn(global, 'setTimeout');
+ mountComponent();
+ });
+
+ it('should call setTimeout on mount and show ObservabilitySkeleton if Observability UI is not loaded yet', () => {
+ jest.runAllTimers();
+
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
+ expect(wrapper.vm.loading).toBe(true);
+ expect(wrapper.vm.timerId).not.toBeNull();
+ });
+
+ it('should call setTimeout on mount and dont show ObservabilitySkeleton if Observability UI is loaded', () => {
+ wrapper.vm.loading = false;
+ jest.runAllTimers();
+
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
+ expect(wrapper.vm.loading).toBe(false);
+ expect(wrapper.vm.timerId).not.toBeNull();
+ });
+ });
+
+ describe('handleSkeleton', () => {
+ it('will not show the skeleton if Observability UI is loaded before', () => {
+ jest.spyOn(global, 'clearTimeout');
+ mountComponent();
+ wrapper.vm.handleSkeleton();
+ expect(clearTimeout).toHaveBeenCalledWith(wrapper.vm.timerId);
+ });
+
+ it('will hide skeleton gracefully after 400ms if skeleton was present on screen before Observability UI', () => {
+ jest.spyOn(global, 'setTimeout');
+ mountComponent();
+ jest.runAllTimers();
+ wrapper.vm.handleSkeleton();
+ jest.runAllTimers();
+
+ expect(setTimeout).toHaveBeenCalledWith(wrapper.vm.hideSkeleton, 400);
+ expect(wrapper.vm.loading).toBe(false);
+ });
+ });
+
+ describe('skeleton variant', () => {
+ it.each`
+ skeletonType | condition | variant
+ ${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANT.DASHBOARDS}
+ ${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANT.EXPLORE}
+ ${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANT.MANAGE}
+ ${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
+ `('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
+ mountComponent({ variant });
+ const showsDefaultSkeleton = ![
+ SKELETON_VARIANT.DASHBOARDS,
+ SKELETON_VARIANT.EXPLORE,
+ SKELETON_VARIANT.MANAGE,
+ ].includes(variant);
+ expect(wrapper.findComponent(DashboardsSkeleton).exists()).toBe(
+ skeletonType === SKELETON_VARIANT.DASHBOARDS,
+ );
+ expect(wrapper.findComponent(ExploreSkeleton).exists()).toBe(
+ skeletonType === SKELETON_VARIANT.EXPLORE,
+ );
+ expect(wrapper.findComponent(ManageSkeleton).exists()).toBe(
+ skeletonType === SKELETON_VARIANT.MANAGE,
+ );
+
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index 4a026f35822..d45b993b5a2 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -6,7 +6,6 @@ import {
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
- DETAILS_IMPORTING_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '~/packages_and_registries/container_registry/explorer/constants';
@@ -77,7 +76,6 @@ describe('Delete alert', () => {
});
});
});
-
describe('error states', () => {
describe.each`
deleteAlertType | message
@@ -107,25 +105,6 @@ describe('Delete alert', () => {
});
});
- describe('importing repository error state', () => {
- beforeEach(() => {
- mountComponent({
- deleteAlertType: 'danger_importing',
- containerRegistryImportingHelpPagePath: 'https://foobar',
- });
- });
-
- it('alert exist and text is appropriate', () => {
- expect(findAlert().text()).toMatchInterpolatedText(DETAILS_IMPORTING_ERROR_MESSAGE);
- });
-
- it('alert body contains link', () => {
- const alertLink = findLink();
- expect(alertLink.exists()).toBe(true);
- expect(alertLink.attributes('href')).toBe('https://foobar');
- });
- });
-
describe('dismissing alert', () => {
it('GlAlert dismiss event triggers a change event', () => {
mountComponent({ deleteAlertType: 'success_tags' });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index b163557618e..1017ff06a25 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -18,7 +18,7 @@ import {
NO_TAGS_MATCHING_FILTERS_TITLE,
NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
} from '~/packages_and_registries/container_registry/explorer/constants/index';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
describe('Tags List', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index b11048cd7a2..e5b99f15e8c 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -249,15 +249,6 @@ export const graphQLDeleteImageRepositoryTagsMock = {
},
};
-export const graphQLDeleteImageRepositoryTagImportingErrorMock = {
- data: {
- destroyContainerRepositoryTags: {
- errors: ['repository importing'],
- __typename: 'DestroyContainerRepositoryTagsPayload',
- },
- },
-};
-
export const dockerCommands = {
dockerBuildCommand: 'foofoo',
dockerPushCommand: 'barbar',
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 310398b01cf..26f0e506829 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -18,7 +18,6 @@ import {
UNFINISHED_STATUS,
DELETE_SCHEDULED,
ALERT_DANGER_IMAGE,
- ALERT_DANGER_IMPORTING,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
MISSING_OR_DELETED_IMAGE_TITLE,
MISSING_OR_DELETED_IMAGE_MESSAGE,
@@ -34,7 +33,6 @@ import Tracking from '~/tracking';
import {
graphQLImageDetailsMock,
graphQLDeleteImageRepositoryTagsMock,
- graphQLDeleteImageRepositoryTagImportingErrorMock,
graphQLProjectImageRepositoriesDetailsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
@@ -341,7 +339,6 @@ describe('Details Page', () => {
const config = {
isAdmin: true,
garbageCollectionHelpPagePath: 'baz',
- containerRegistryImportingHelpPagePath: 'https://foobar',
};
const deleteAlertType = 'success_tag';
@@ -366,38 +363,6 @@ describe('Details Page', () => {
expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
});
-
- describe('importing repository error', () => {
- let mutationResolver;
- let tagsResolver;
- let detailsResolver;
-
- beforeEach(async () => {
- mutationResolver = jest
- .fn()
- .mockResolvedValue(graphQLDeleteImageRepositoryTagImportingErrorMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
- detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
-
- mountComponent({ mutationResolver, tagsResolver, detailsResolver });
- await waitForApolloRequestRender();
- });
-
- it('displays the proper alert', async () => {
- findTagsList().vm.$emit('delete', [cleanTags[0]]);
- await nextTick();
-
- findDeleteModal().vm.$emit('confirmDelete');
- await waitForPromises();
-
- expect(tagsResolver).toHaveBeenCalled();
- expect(detailsResolver).toHaveBeenCalled();
-
- const deleteAlert = findDeleteAlert();
- expect(deleteAlert.exists()).toBe(true);
- expect(deleteAlert.props('deleteAlertType')).toBe(ALERT_DANGER_IMPORTING);
- });
- });
});
describe('Partial Cleanup Alert', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index 79403d29d18..1e514d85e82 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -6,7 +6,6 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
import GroupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
@@ -23,6 +22,7 @@ import getContainerRepositoriesDetails from '~/packages_and_registries/container
import component from '~/packages_and_registries/container_registry/explorer/pages/list.vue';
import Tracking from '~/tracking';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { $toast } from 'jest/packages_and_registries/shared/mocks';
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index fb50d623543..329cc15df97 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -14,7 +14,6 @@ import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import axios from '~/lib/utils/axios_utils';
@@ -190,7 +189,7 @@ describe('DependencyProxyApp', () => {
it('shows list', () => {
expect(findManifestList().props()).toMatchObject({
manifests: proxyManifests(),
- pagination: stripTypenames(pagination()),
+ pagination: pagination(),
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
index 9e4c747a1bd..2f415bfd6f9 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -1,5 +1,4 @@
import { GlKeysetPagination } from '@gitlab/ui';
-import { stripTypenames } from 'helpers/graphql_helpers';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
@@ -14,7 +13,7 @@ describe('Manifests List', () => {
const defaultProps = {
manifests: proxyManifests(),
- pagination: stripTypenames(pagination()),
+ pagination: pagination(),
};
const createComponent = (propsData = defaultProps) => {
@@ -60,9 +59,8 @@ describe('Manifests List', () => {
it('has the correct props', () => {
createComponent();
- expect(findPagination().props()).toMatchObject({
- ...defaultProps.pagination,
- });
+ const { __typename, ...paginationProps } = defaultProps.pagination;
+ expect(findPagination().props()).toMatchObject(paginationProps);
});
it('emits the next-page event', () => {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
index 8fd50bea280..69765d31674 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
@@ -8,7 +8,7 @@ import ArtifactsList from '~/packages_and_registries/harbor_registry/components/
import waitForPromises from 'helpers/wait_for_promises';
import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import {
NAME_SORT_FIELD,
TOKEN_TYPE_TAG_NAME,
@@ -137,7 +137,7 @@ describe('Harbor Details Page', () => {
title: s__('HarborRegistry|Tag'),
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
],
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index dff95364d7d..d237023d0cd 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -7,13 +7,11 @@ import { createAlert, VARIANT_INFO } from '~/flash';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
-import {
- SHOW_DELETE_SUCCESS_ALERT,
- FILTERED_SEARCH_TERM,
-} from '~/packages_and_registries/shared/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
import * as packageUtils from '~/packages_and_registries/shared/utils';
import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 92c2cd90568..c4020eeb75f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -13,7 +13,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<div>
<div
- class="dropdown b-dropdown gl-new-dropdown btn-group"
+ class="dropdown b-dropdown gl-dropdown btn-group"
id="__BVID__27"
lazy=""
>
@@ -30,7 +30,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<!---->
<span
- class="gl-new-dropdown-button-text"
+ class="gl-dropdown-button-text"
>
Show PyPi commands
</span>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 913b4f5926f..bb04701a8b7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,4 +1,4 @@
-import { GlFormCheckbox, GlSprintf } from '@gitlab/ui';
+import { GlFormCheckbox, GlSprintf, GlTruncate } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -15,7 +15,13 @@ import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.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';
+import {
+ linksData,
+ packageData,
+ packagePipelines,
+ packageProject,
+ packageTags,
+} from '../../mock_data';
Vue.use(VueRouter);
@@ -26,9 +32,9 @@ describe('packages_list_row', () => {
isGroupPage: false,
};
- const packageWithoutTags = { ...packageData(), project: packageProject() };
+ const packageWithoutTags = { ...packageData(), project: packageProject(), ...linksData };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
- const packageCannotDestroy = { ...packageData(), canDestroy: false };
+ const packageCannotDestroy = { ...packageData(), ...linksData, canDestroy: false };
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackagePath = () => wrapper.findComponent(PackagePath);
@@ -41,6 +47,7 @@ describe('packages_list_row', () => {
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
+ const findPackageName = () => wrapper.findComponent(GlTruncate);
const mountComponent = ({
packageEntity = packageWithoutTags,
@@ -81,6 +88,22 @@ describe('packages_list_row', () => {
});
});
+ it('does not have a link to navigate to the details page', () => {
+ mountComponent({
+ packageEntity: {
+ ...packageWithoutTags,
+ _links: {
+ webPath: null,
+ },
+ },
+ });
+
+ expect(findPackageLink().exists()).toBe(false);
+ expect(findPackageName().props()).toMatchObject({
+ text: '@gitlab-org/package-15',
+ });
+ });
+
describe('tags', () => {
it('renders package tags when a package has tags', () => {
mountComponent({ packageEntity: packageWithTags });
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 19505618ff7..a884959ab62 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
@@ -10,6 +10,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+import { TOKEN_TYPE_TYPE } from '~/vue_shared/components/filtered_search_bar/constants';
jest.mock('~/packages_and_registries/shared/utils');
@@ -92,7 +93,11 @@ describe('Package Search', () => {
expect(findRegistrySearch().props()).toMatchObject({
tokens: expect.arrayContaining([
- expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ expect.objectContaining({
+ token: PackageTypeToken,
+ type: TOKEN_TYPE_TYPE,
+ icon: 'package',
+ }),
]),
sortableFields: sortableFields(isGroupPage),
});
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 f36c5923532..9e9e08bc196 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -118,6 +118,13 @@ export const packageVersions = () => [
},
];
+export const linksData = {
+ _links: {
+ webPath: '/gitlab-org/package-15',
+ __typeName: 'PackageLinks',
+ },
+};
+
export const packageData = (extend) => ({
__typename: 'Package',
id: 'gid://gitlab/Packages::Package/111',
@@ -232,6 +239,7 @@ export const packageDetailsQuery = (extendPackage) => ({
__typename: 'PackageFileConnection',
},
versions: {
+ count: packageVersions().length,
nodes: packageVersions(),
pageInfo: {
hasNextPage: true,
@@ -376,6 +384,7 @@ export const packagesListQuery = ({ type = 'group', extend = {}, extendPaginatio
nodes: [
{
...packageData(),
+ ...linksData,
project: packageProject(),
tags: { nodes: packageTags() },
pipelines: {
@@ -387,6 +396,7 @@ export const packagesListQuery = ({ type = 'group', extend = {}, extendPaginatio
project: packageProject(),
tags: { nodes: [] },
pipelines: { nodes: [] },
+ ...linksData,
},
],
pageInfo: pagination(extendPagination),
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index f942a334f40..eb3b999c1ca 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlBadge, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -42,6 +42,7 @@ import {
packageFiles,
packageDestroyFilesMutation,
packageDestroyFilesMutationError,
+ pagination,
} from '../mock_data';
jest.mock('~/flash');
@@ -122,7 +123,9 @@ describe('PackagesApp', () => {
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findVersionsList = () => wrapper.findComponent(PackageVersionsList);
- const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
+ const findVersionsCountBadge = () => wrapper.findByTestId('other-versions-badge');
+ const findNoVersionsMessage = () => wrapper.findByTestId('no-versions-message');
+ const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
const findDeletePackage = () => wrapper.findComponent(DeletePackage);
@@ -564,6 +567,30 @@ describe('PackagesApp', () => {
await waitForPromises();
expect(findVersionsList()).toBeDefined();
+ expect(findVersionsCountBadge().exists()).toBe(true);
+ expect(findVersionsCountBadge().text()).toBe(packageVersions().length.toString());
+ });
+
+ it('displays tab with 0 count when package has no other versions', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ versions: {
+ count: 0,
+ nodes: [],
+ pageInfo: pagination({ hasNextPage: false, hasPreviousPage: false }),
+ },
+ }),
+ ),
+ });
+
+ await waitForPromises();
+
+ expect(findVersionsCountBadge().exists()).toBe(true);
+ expect(findVersionsCountBadge().text()).toBe('0');
+ expect(findNoVersionsMessage().text()).toMatchInterpolatedText(
+ 'There are no other versions of this package.',
+ );
});
it('binds the correct props', async () => {
@@ -576,6 +603,7 @@ describe('PackagesApp', () => {
});
});
});
+
describe('dependency links', () => {
it('does not show the dependency links for a non nuget package', async () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index 8e08864bdb8..cbb5aa52694 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -232,6 +232,7 @@ describe('Container Expiration Policy Settings Form', () => {
describe('form', () => {
describe('form submit event', () => {
useMockLocationHelper();
+ const originalHref = window.location.href;
it('save has type submit', () => {
mountComponent();
@@ -319,7 +320,7 @@ describe('Container Expiration Policy Settings Form', () => {
await submitForm();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
- expect(window.location.href).toBeUndefined();
+ expect(window.location.href).toBe(originalHref);
});
it('parses the error messages', async () => {
diff --git a/spec/frontend/packages_and_registries/shared/utils_spec.js b/spec/frontend/packages_and_registries/shared/utils_spec.js
index 962cb2257ce..d81cdbfd8bd 100644
--- a/spec/frontend/packages_and_registries/shared/utils_spec.js
+++ b/spec/frontend/packages_and_registries/shared/utils_spec.js
@@ -1,4 +1,3 @@
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import {
getQueryParams,
keyValueToFilterToken,
@@ -7,6 +6,7 @@ import {
beautifyPath,
getCommitLink,
} from '~/packages_and_registries/shared/utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import { packageList } from 'jest/packages_and_registries/infrastructure_registry/components/mock_data';
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 03aed7454e3..825aef27327 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -4,7 +4,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
-import { visitUrl } from '~/lib/utils/url_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -15,12 +14,10 @@ const TEST_COUNT_BIG = 2000;
const TEST_DONE_COUNT_BIG = 7300;
describe('Todos', () => {
- let todoItem;
let mock;
beforeEach(() => {
loadHTMLFixture('todos/todos.html');
- todoItem = document.querySelector('.todos-list .todo');
mock = new MockAdapter(axios);
return new Todos();
@@ -34,95 +31,47 @@ describe('Todos', () => {
mock.restore();
});
- describe('goToTodoUrl', () => {
- it('opens the todo url', () => {
- const todoLink = todoItem.dataset.url;
+ describe('on done todo click', () => {
+ let onToggleSpy;
- let expectedUrl = null;
- visitUrl.mockImplementation((url) => {
- expectedUrl = url;
- });
+ beforeEach(() => {
+ const el = document.querySelector('.js-done-todo');
+ const path = el.dataset.href;
- todoItem.click();
+ // Arrange
+ mock
+ .onDelete(path)
+ .replyOnce(200, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
+ onToggleSpy = jest.fn();
+ document.addEventListener('todo:toggle', onToggleSpy);
- expect(expectedUrl).toEqual(todoLink);
- });
-
- describe('meta click', () => {
- let windowOpenSpy;
- let metakeyEvent;
-
- beforeEach(() => {
- metakeyEvent = new MouseEvent('click', { ctrlKey: true });
- windowOpenSpy = jest.spyOn(window, 'open').mockImplementation(() => {});
- });
-
- it('opens the todo url in another tab', () => {
- const todoLink = todoItem.dataset.url;
-
- document.querySelectorAll('.todos-list .todo').forEach((el) => {
- el.dispatchEvent(metakeyEvent);
- });
-
- expect(visitUrl).not.toHaveBeenCalled();
- expect(windowOpenSpy).toHaveBeenCalledWith(todoLink, '_blank');
- });
-
- it('run native funcionality when avatar is clicked', () => {
- document.querySelectorAll('.todos-list a').forEach((el) => {
- el.addEventListener('click', (e) => e.preventDefault());
- });
- document.querySelectorAll('.todos-list img').forEach((el) => {
- el.dispatchEvent(metakeyEvent);
- });
+ // Act
+ el.click();
- expect(visitUrl).not.toHaveBeenCalled();
- expect(windowOpenSpy).not.toHaveBeenCalled();
- });
+ // Wait for axios and HTML to udpate
+ return waitForPromises();
});
- describe('on done todo click', () => {
- let onToggleSpy;
-
- beforeEach(() => {
- const el = document.querySelector('.js-done-todo');
- const path = el.dataset.href;
-
- // Arrange
- mock
- .onDelete(path)
- .replyOnce(200, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
- onToggleSpy = jest.fn();
- document.addEventListener('todo:toggle', onToggleSpy);
-
- // Act
- el.click();
-
- // Wait for axios and HTML to udpate
- return waitForPromises();
- });
-
- it('dispatches todo:toggle', () => {
- expect(onToggleSpy).toHaveBeenCalledWith(
- expect.objectContaining({
- detail: {
- count: TEST_COUNT_BIG,
- },
- }),
- );
- });
+ it('dispatches todo:toggle', () => {
+ expect(onToggleSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ detail: {
+ count: TEST_COUNT_BIG,
+ },
+ }),
+ );
+ });
- it('updates pending text', () => {
- expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
- addDelimiter(TEST_COUNT_BIG),
- );
- });
+ it('updates pending text', () => {
+ expect(document.querySelector('.js-todos-pending .js-todos-badge').innerHTML).toEqual(
+ addDelimiter(TEST_COUNT_BIG),
+ );
+ });
- it('updates done text', () => {
- expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
- addDelimiter(TEST_DONE_COUNT_BIG),
- );
- });
+ it('updates done text', () => {
+ expect(document.querySelector('.js-todos-done .js-todos-badge').innerHTML).toEqual(
+ addDelimiter(TEST_DONE_COUNT_BIG),
+ );
});
});
});
diff --git a/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
index c1e1545944b..d60730e630b 100644
--- a/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
+++ b/spec/frontend/pages/import/fogbugz/new_user_map/components/user_select_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
@@ -59,7 +59,7 @@ describe('fogbugz user select component', () => {
const id = 8;
- wrapper.findComponent(GlListbox).vm.$emit('select', `gid://gitlab/User/${id}`);
+ wrapper.findComponent(GlCollapsibleListbox).vm.$emit('select', `gid://gitlab/User/${id}`);
await nextTick();
expect(wrapper.get('input').attributes('value')).toBe(id.toString());
@@ -69,7 +69,7 @@ describe('fogbugz user select component', () => {
createComponent();
jest.runOnlyPendingTimers();
- wrapper.findComponent(GlListbox).vm.$emit('search', 'test');
+ wrapper.findComponent(GlCollapsibleListbox).vm.$emit('search', 'test');
await nextTick();
jest.runOnlyPendingTimers();
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index 727c5164cdc..9718d847ed5 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -1,5 +1,5 @@
import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
-import { getByRole, getAllByRole } from '@testing-library/dom';
+import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -133,10 +133,15 @@ describe('ForkForm component', () => {
expect(cancelButton.attributes('href')).toBe(projectFullPath);
});
- const selectedMockNamespace = { name: 'two', full_name: 'two-group/two', id: 2 };
+ const selectedMockNamespace = {
+ name: 'two',
+ full_name: 'two-group/two',
+ id: 2,
+ visibility: 'public',
+ };
- const fillForm = () => {
- findForkUrlInput().vm.$emit('select', selectedMockNamespace);
+ const fillForm = (namespace = selectedMockNamespace) => {
+ findForkUrlInput().vm.$emit('select', namespace);
};
it('has input with csrf token', () => {
@@ -226,66 +231,139 @@ describe('ForkForm component', () => {
},
];
- it('resets the visibility to default "private"', async () => {
+ it('resets the visibility to max allowed below current level', async () => {
+ createFullComponent({ projectVisibility: 'public' }, { namespaces });
+
+ expect(wrapper.vm.form.fields.visibility.value).toBe('public');
+
+ fillForm({
+ name: 'one',
+ id: 1,
+ visibility: 'internal',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /internal/i }).checked).toBe(true);
+ });
+
+ it('does not reset the visibility when current level is allowed', async () => {
+ createFullComponent({ projectVisibility: 'public' }, { namespaces });
+
+ expect(wrapper.vm.form.fields.visibility.value).toBe('public');
+
+ fillForm({
+ name: 'two',
+ id: 2,
+ visibility: 'public',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /public/i }).checked).toBe(true);
+ });
+
+ it('does not reset the visibility when visibility cap is increased', async () => {
createFullComponent({ projectVisibility: 'public' }, { namespaces });
expect(wrapper.vm.form.fields.visibility.value).toBe('public');
- fillForm();
+ fillForm({
+ name: 'three',
+ id: 3,
+ visibility: 'internal',
+ });
+ await nextTick();
+
+ fillForm({
+ name: 'four',
+ id: 4,
+ visibility: 'public',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /internal/i }).checked).toBe(true);
+ });
+
+ it('sets the visibility to be next highest from current when restrictedVisibilityLevels is set', async () => {
+ createFullComponent(
+ { projectVisibility: 'public', restrictedVisibilityLevels: [10] },
+ { namespaces },
+ );
+
+ wrapper.vm.form.fields.visibility.value = 'internal';
+ fillForm({
+ name: 'five',
+ id: 5,
+ visibility: 'public',
+ });
await nextTick();
expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
});
- it('sets the visibility to be null when restrictedVisibilityLevels is set', async () => {
- createFullComponent({ restrictedVisibilityLevels: [10] }, { namespaces });
+ it('sets the visibility to be next lowest from current when nothing lower is allowed', async () => {
+ createFullComponent(
+ { projectVisibility: 'public', restrictedVisibilityLevels: [0] },
+ { namespaces },
+ );
+
+ fillForm({
+ name: 'six',
+ id: 6,
+ visibility: 'private',
+ });
+ await nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
- fillForm();
+ fillForm({
+ name: 'six',
+ id: 6,
+ visibility: 'public',
+ });
await nextTick();
- const container = getByRole(wrapper.element, 'radiogroup', { name: /visibility/i });
- const visibilityRadios = getAllByRole(container, 'radio');
- expect(visibilityRadios.filter((e) => e.checked)).toHaveLength(0);
+ expect(getByRole(wrapper.element, 'radio', { name: /internal/i }).checked).toBe(true);
});
});
it.each`
- project | restrictedVisibilityLevels
- ${'private'} | ${[]}
- ${'internal'} | ${[]}
- ${'public'} | ${[]}
- ${'private'} | ${[0]}
- ${'private'} | ${[10]}
- ${'private'} | ${[20]}
- ${'private'} | ${[0, 10]}
- ${'private'} | ${[0, 20]}
- ${'private'} | ${[10, 20]}
- ${'private'} | ${[0, 10, 20]}
- ${'internal'} | ${[0]}
- ${'internal'} | ${[10]}
- ${'internal'} | ${[20]}
- ${'internal'} | ${[0, 10]}
- ${'internal'} | ${[0, 20]}
- ${'internal'} | ${[10, 20]}
- ${'internal'} | ${[0, 10, 20]}
- ${'public'} | ${[0]}
- ${'public'} | ${[10]}
- ${'public'} | ${[0, 10]}
- ${'public'} | ${[0, 20]}
- ${'public'} | ${[10, 20]}
- ${'public'} | ${[0, 10, 20]}
- `('checks the correct radio button', ({ project, restrictedVisibilityLevels }) => {
- createFullComponent({
- projectVisibility: project,
- restrictedVisibilityLevels,
- });
+ project | restrictedVisibilityLevels | computedVisibilityLevel
+ ${'private'} | ${[]} | ${'private'}
+ ${'internal'} | ${[]} | ${'internal'}
+ ${'public'} | ${[]} | ${'public'}
+ ${'private'} | ${[0]} | ${'private'}
+ ${'private'} | ${[10]} | ${'private'}
+ ${'private'} | ${[20]} | ${'private'}
+ ${'private'} | ${[0, 10]} | ${'private'}
+ ${'private'} | ${[0, 20]} | ${'private'}
+ ${'private'} | ${[10, 20]} | ${'private'}
+ ${'private'} | ${[0, 10, 20]} | ${'private'}
+ ${'internal'} | ${[0]} | ${'internal'}
+ ${'internal'} | ${[10]} | ${'private'}
+ ${'internal'} | ${[20]} | ${'internal'}
+ ${'internal'} | ${[0, 10]} | ${'private'}
+ ${'internal'} | ${[0, 20]} | ${'internal'}
+ ${'internal'} | ${[10, 20]} | ${'private'}
+ ${'internal'} | ${[0, 10, 20]} | ${'private'}
+ ${'public'} | ${[0]} | ${'public'}
+ ${'public'} | ${[10]} | ${'public'}
+ ${'public'} | ${[0, 10]} | ${'public'}
+ ${'public'} | ${[0, 20]} | ${'internal'}
+ ${'public'} | ${[10, 20]} | ${'private'}
+ ${'public'} | ${[0, 10, 20]} | ${'private'}
+ `(
+ 'checks the correct radio button',
+ ({ project, restrictedVisibilityLevels, computedVisibilityLevel }) => {
+ createFullComponent({
+ projectVisibility: project,
+ restrictedVisibilityLevels,
+ });
- if (restrictedVisibilityLevels.length === 0) {
- expect(wrapper.find('[name="visibility"]:checked').attributes('value')).toBe(project);
- } else {
- expect(wrapper.find('[name="visibility"]:checked').exists()).toBe(false);
- }
- });
+ expect(wrapper.find('[name="visibility"]:checked').attributes('value')).toBe(
+ computedVisibilityLevel,
+ );
+ },
+ );
it.each`
project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled | restrictedVisibilityLevels
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 21a38f066d9..e7c7ec0d336 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
@@ -36,62 +36,48 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<!---->
- <gl-dropdown-stub
+ <gl-base-dropdown-stub
+ ariahaspopup="listbox"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
size="medium"
- text="rspec"
+ toggleid="dropdown-toggle-btn-6"
+ toggletext="rspec"
variant="default"
>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischecked="true"
- ischeckitem="true"
- secondarytext=""
- value="rspec"
- >
-
- rspec
-
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckitem="true"
- secondarytext=""
- value="cypress"
+ <!---->
+
+ <!---->
+
+ <ul
+ aria-labelledby="dropdown-toggle-btn-6"
+ class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
+ id="listbox"
+ role="listbox"
+ tabindex="-1"
>
-
- cypress
-
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckitem="true"
- secondarytext=""
- value="karma"
- >
-
- karma
-
- </gl-dropdown-item-stub>
- </gl-dropdown-stub>
+ <gl-listbox-item-stub
+ isselected="true"
+ >
+
+ rspec
+
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub>
+
+ cypress
+
+ </gl-listbox-item-stub>
+ <gl-listbox-item-stub>
+
+ karma
+
+ </gl-listbox-item-stub>
+ </ul>
+
+ <!---->
+
+ </gl-base-dropdown-stub>
</div>
<gl-area-chart-stub
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 2f2edd6b025..e99734963e3 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlAlert, GlListbox, GlListboxItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -22,9 +22,10 @@ describe('Code Coverage', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findAreaChart = () => wrapper.findComponent(GlAreaChart);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstDropdownItem = () => findAllDropdownItems().at(0);
- const findSecondDropdownItem = () => findAllDropdownItems().at(1);
+ const findListBox = () => wrapper.findComponent(GlListbox);
+ const findListBoxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findFirstListBoxItem = () => findListBoxItems().at(0);
+ const findSecondListBoxItem = () => findListBoxItems().at(1);
const findDownloadButton = () => wrapper.find('[data-testid="download-button"]');
const createComponent = () => {
@@ -36,6 +37,7 @@ describe('Code Coverage', () => {
graphRef,
graphCsvPath,
},
+ stubs: { GlListbox },
});
};
@@ -142,9 +144,9 @@ describe('Code Coverage', () => {
});
it('renders the dropdown with all custom names as options', () => {
- expect(wrapper.findComponent(GlDropdown).exists()).toBeDefined();
- expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
- expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
+ expect(findListBox().exists()).toBe(true);
+ expect(findListBoxItems()).toHaveLength(codeCoverageMockData.length);
+ expect(findFirstListBoxItem().text()).toBe(codeCoverageMockData[0].group_name);
});
});
@@ -159,19 +161,19 @@ describe('Code Coverage', () => {
});
it('updates the selected dropdown option with an icon', async () => {
- findSecondDropdownItem().vm.$emit('click');
+ findListBox().vm.$emit('select', '1');
await nextTick();
- expect(findFirstDropdownItem().attributes('ischecked')).toBe(undefined);
- expect(findSecondDropdownItem().attributes('ischecked')).toBe('true');
+ expect(findFirstListBoxItem().attributes('isselected')).toBeUndefined();
+ expect(findSecondListBoxItem().attributes('isselected')).toBe('true');
});
it('updates the graph data when selecting a different option in dropdown', async () => {
const originalSelectedData = wrapper.vm.selectedDailyCoverage;
const expectedData = codeCoverageMockData[1];
- findSecondDropdownItem().vm.$emit('click');
+ findListBox().vm.$emit('select', '1');
await nextTick();
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
deleted file mode 100644
index 4cac642bb50..00000000000
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { formatUtcOffset, formatTimezone } from '~/lib/utils/datetime_utility';
-import { findTimezoneByIdentifier } from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
-
-describe('Timezone Dropdown', () => {
- describe('formatUtcOffset', () => {
- it('will convert negative utc offsets in seconds to hours and minutes', () => {
- expect(formatUtcOffset(-21600)).toEqual('- 6');
- });
-
- it('will convert positive utc offsets in seconds to hours and minutes', () => {
- expect(formatUtcOffset(25200)).toEqual('+ 7');
- expect(formatUtcOffset(49500)).toEqual('+ 13.75');
- });
-
- it('will return 0 when given a string', () => {
- expect(formatUtcOffset('BLAH')).toEqual('0');
- expect(formatUtcOffset('$%$%')).toEqual('0');
- });
-
- it('will return 0 when given an array', () => {
- expect(formatUtcOffset(['an', 'array'])).toEqual('0');
- });
-
- it('will return 0 when given an object', () => {
- expect(formatUtcOffset({ some: '', object: '' })).toEqual('0');
- });
-
- it('will return 0 when given null', () => {
- expect(formatUtcOffset(null)).toEqual('0');
- });
-
- it('will return 0 when given undefined', () => {
- expect(formatUtcOffset(undefined)).toEqual('0');
- });
-
- it('will return 0 when given empty input', () => {
- expect(formatUtcOffset('')).toEqual('0');
- });
- });
-
- describe('formatTimezone', () => {
- it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => {
- expect(
- formatTimezone({
- name: 'Chatham Is.',
- offset: 49500,
- identifier: 'Pacific/Chatham',
- }),
- ).toEqual('[UTC + 13.75] Chatham Is.');
- });
-
- it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => {
- expect(
- formatTimezone({
- name: 'Saskatchewan',
- offset: -21600,
- identifier: 'America/Regina',
- }),
- ).toEqual('[UTC - 6] Saskatchewan');
- });
-
- it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => {
- expect(
- formatTimezone({
- name: 'Accra',
- offset: 0,
- identifier: 'Africa/Accra',
- }),
- ).toEqual('[UTC 0] Accra');
- });
- });
-
- describe('findTimezoneByIdentifier', () => {
- const tzList = [
- {
- identifier: 'Asia/Tokyo',
- name: 'Sapporo',
- offset: 32400,
- },
- {
- identifier: 'Asia/Hong_Kong',
- name: 'Hong Kong',
- offset: 28800,
- },
- {
- identifier: 'Asia/Dhaka',
- name: 'Dhaka',
- offset: 21600,
- },
- ];
-
- const identifier = 'Asia/Dhaka';
- it('returns the correct object if the identifier exists', () => {
- const res = findTimezoneByIdentifier(tzList, identifier);
-
- expect(res).toBe(tzList[2]);
- });
-
- it('returns null if it doesnt find the identifier', () => {
- const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne');
-
- expect(res).toBeNull();
- });
-
- it('returns null if there is no identifier given', () => {
- expect(findTimezoneByIdentifier(tzList)).toBeNull();
- expect(findTimezoneByIdentifier(tzList, '')).toBeNull();
- });
-
- it('returns null if there is an empty or invalid array given', () => {
- expect(findTimezoneByIdentifier([], identifier)).toBeNull();
- expect(findTimezoneByIdentifier(null, identifier)).toBeNull();
- expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull();
- });
- });
-});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index b202a148306..38f7a2e919d 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -24,7 +24,6 @@ const defaultProps = {
buildsAccessLevel: 20,
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
- operationsAccessLevel: 20,
metricsDashboardAccessLevel: 20,
pagesAccessLevel: 10,
analyticsAccessLevel: 20,
@@ -114,9 +113,14 @@ describe('Settings Panel', () => {
const findPackageSettings = () => wrapper.findComponent({ ref: 'package-settings' });
const findPackageAccessLevel = () =>
wrapper.find('[data-testid="package-registry-access-level"]');
- const findPackageAccessLevels = () =>
- wrapper.find('[name="project[project_feature_attributes][package_registry_access_level]"]');
const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
+ const findPackageRegistryEnabledInput = () => wrapper.find('[name="package_registry_enabled"]');
+ const findPackageRegistryAccessLevelHiddenInput = () =>
+ wrapper.find(
+ 'input[name="project[project_feature_attributes][package_registry_access_level]"]',
+ );
+ const findPackageRegistryApiForEveryoneEnabledInput = () =>
+ wrapper.find('[name="package_registry_api_for_everyone_enabled"]');
const findPagesSettings = () => wrapper.findComponent({ ref: 'pages-settings' });
const findPagesAccessLevels = () =>
wrapper.find('[name="project[project_feature_attributes][pages_access_level]"]');
@@ -131,9 +135,6 @@ describe('Settings Panel', () => {
wrapper.findComponent({ ref: 'metrics-visibility-settings' });
const findMetricsVisibilityInput = () =>
findMetricsVisibilitySettings().findComponent(ProjectFeatureSetting);
- const findOperationsSettings = () => wrapper.findComponent({ ref: 'operations-settings' });
- const findOperationsVisibilityInput = () =>
- findOperationsSettings().findComponent(ProjectFeatureSetting);
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findFeatureFlagsSettings = () => wrapper.findComponent({ ref: 'feature-flags-settings' });
@@ -141,6 +142,8 @@ describe('Settings Panel', () => {
wrapper.findComponent({ ref: 'infrastructure-settings' });
const findReleasesSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' });
+ const findMonitorVisibilityInput = () =>
+ findMonitorSettings().findComponent(ProjectFeatureSetting);
afterEach(() => {
wrapper.destroy();
@@ -283,7 +286,7 @@ describe('Settings Panel', () => {
});
expect(findRepositoryFeatureProjectRow().props('helpText')).toBe(
- 'View and edit files in this project. Non-project members have only read access.',
+ 'View and edit files in this project. When set to **Everyone With Access** non-project members have only read access.',
);
});
});
@@ -587,28 +590,63 @@ describe('Settings Panel', () => {
expect(findPackageAccessLevel().exists()).toBe(true);
});
+ it('has hidden input field for package registry access level', () => {
+ wrapper = mountComponent({
+ glFeatures: { packageRegistryAccessLevel: true },
+ packagesAvailable: true,
+ });
+
+ expect(findPackageRegistryAccessLevelHiddenInput().exists()).toBe(true);
+ });
+
it.each`
- visibilityLevel | output
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[[30, 'Everyone']]}
+ projectVisibilityLevel | packageRegistryEnabled | packageRegistryApiForEveryoneEnabled | expectedAccessLevel
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${false} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${false} | ${featureAccessLevel.EVERYONE}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${false} | ${'hidden'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${true} | ${'hidden'} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
`(
- 'renders correct options when visibilityLevel is $visibilityLevel',
- async ({ visibilityLevel, output }) => {
+ 'sets correct access level',
+ async ({
+ projectVisibilityLevel,
+ packageRegistryEnabled,
+ packageRegistryApiForEveryoneEnabled,
+ expectedAccessLevel,
+ }) => {
wrapper = mountComponent({
glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
currentSettings: {
- visibilityLevel,
+ visibilityLevel: projectVisibilityLevel,
},
});
- expect(findPackageAccessLevels().props('options')).toStrictEqual(output);
+ await findPackageRegistryEnabledInput().vm.$emit('change', packageRegistryEnabled);
+
+ const packageRegistryApiForEveryoneEnabledInput = findPackageRegistryApiForEveryoneEnabledInput();
+
+ if (packageRegistryApiForEveryoneEnabled === 'hidden') {
+ expect(packageRegistryApiForEveryoneEnabledInput.exists()).toBe(false);
+ } else if (packageRegistryApiForEveryoneEnabled === 'disabled') {
+ expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(true);
+ } else {
+ expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(false);
+ await packageRegistryApiForEveryoneEnabledInput.vm.$emit(
+ 'change',
+ packageRegistryApiForEveryoneEnabled,
+ );
+ }
+
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
},
);
it.each`
- initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption
+ initialProjectVisibilityLevel | newProjectVisibilityLevel | initialAccessLevel | expectedAccessLevel
${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE}
${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
@@ -626,27 +664,25 @@ describe('Settings Panel', () => {
${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE}
`(
- 'changes option from $initialPackageRegistryOption to $expectedPackageRegistryOption when visibilityLevel changed from $initialProjectVisibilityLevel to $newProjectVisibilityLevel',
+ 'changes access level when project visibility level changed',
async ({
initialProjectVisibilityLevel,
newProjectVisibilityLevel,
- initialPackageRegistryOption,
- expectedPackageRegistryOption,
+ initialAccessLevel,
+ expectedAccessLevel,
}) => {
wrapper = mountComponent({
glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
currentSettings: {
visibilityLevel: initialProjectVisibilityLevel,
- packageRegistryAccessLevel: initialPackageRegistryOption,
+ packageRegistryAccessLevel: initialAccessLevel,
},
});
await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel);
- expect(findPackageAccessLevels().props('value')).toStrictEqual(
- expectedPackageRegistryOption,
- );
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
},
);
});
@@ -751,27 +787,27 @@ describe('Settings Panel', () => {
${featureAccessLevel.EVERYONE} | ${featureAccessLevel.NOT_ENABLED}
${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.NOT_ENABLED}
`(
- 'when updating Operations Settings access level from `$before` to `$after`, Metric Dashboard access is updated to `$after` as well',
+ 'when updating Monitor access level from `$before` to `$after`, Metric Dashboard access is updated to `$after` as well',
async ({ before, after }) => {
wrapper = mountComponent({
- currentSettings: { operationsAccessLevel: before, metricsDashboardAccessLevel: before },
+ currentSettings: { monitorAccessLevel: before, metricsDashboardAccessLevel: before },
});
- await findOperationsVisibilityInput().vm.$emit('change', after);
+ await findMonitorVisibilityInput().vm.$emit('change', after);
expect(findMetricsVisibilityInput().props('value')).toBe(after);
},
);
- it('when updating Operations Settings access level from `10` to `20`, Metric Dashboard access is not increased', async () => {
+ it('when updating Monitor access level from `10` to `20`, Metric Dashboard access is not increased', async () => {
wrapper = mountComponent({
currentSettings: {
- operationsAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
+ monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
},
});
- await findOperationsVisibilityInput().vm.$emit('change', featureAccessLevel.EVERYONE);
+ await findMonitorVisibilityInput().vm.$emit('change', featureAccessLevel.EVERYONE);
expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS);
});
@@ -780,7 +816,7 @@ describe('Settings Panel', () => {
wrapper = mountComponent({
currentSettings: {
visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER,
- operationsAccessLevel: featureAccessLevel.EVERYONE,
+ monitorAccessLevel: featureAccessLevel.EVERYONE,
metricsDashboardAccessLevel: featureAccessLevel.EVERYONE,
},
});
@@ -799,84 +835,32 @@ describe('Settings Panel', () => {
});
});
- describe('Operations', () => {
- it('should show the operations toggle', () => {
- wrapper = mountComponent();
-
- expect(findOperationsSettings().exists()).toBe(true);
- });
- });
-
describe('Environments', () => {
- describe('with feature flag', () => {
- it('should show the environments toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
+ it('should show the environments toggle', () => {
+ wrapper = mountComponent({});
- expect(findEnvironmentsSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the environments toggle', () => {
- wrapper = mountComponent({});
-
- expect(findEnvironmentsSettings().exists()).toBe(false);
- });
+ expect(findEnvironmentsSettings().exists()).toBe(true);
});
});
describe('Feature Flags', () => {
- describe('with feature flag', () => {
- it('should show the feature flags toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
-
- expect(findFeatureFlagsSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the feature flags toggle', () => {
- wrapper = mountComponent({});
+ it('should show the feature flags toggle', () => {
+ wrapper = mountComponent({});
- expect(findFeatureFlagsSettings().exists()).toBe(false);
- });
+ expect(findFeatureFlagsSettings().exists()).toBe(true);
});
});
describe('Infrastructure', () => {
- describe('with feature flag', () => {
- it('should show the infrastructure toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
+ it('should show the infrastructure toggle', () => {
+ wrapper = mountComponent({});
- expect(findInfrastructureSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the infrastructure toggle', () => {
- wrapper = mountComponent({});
-
- expect(findInfrastructureSettings().exists()).toBe(false);
- });
+ expect(findInfrastructureSettings().exists()).toBe(true);
});
});
describe('Releases', () => {
- describe('with feature flag', () => {
- it('should show the releases toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
+ it('should show the releases toggle', () => {
+ wrapper = mountComponent({});
- expect(findReleasesSettings().exists()).toBe(true);
- });
- });
- describe('without feature flag', () => {
- it('should not show the releases toggle', () => {
- wrapper = mountComponent({});
-
- expect(findReleasesSettings().exists()).toBe(false);
- });
+ expect(findReleasesSettings().exists()).toBe(true);
});
});
describe('Monitor', () => {
@@ -884,37 +868,20 @@ describe('Settings Panel', () => {
[10, 'Only Project Members'],
[20, 'Everyone With Access'],
];
- describe('with feature flag', () => {
- it('shows Monitor toggle instead of Operations toggle', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- });
-
- expect(findMonitorSettings().exists()).toBe(true);
- expect(findOperationsSettings().exists()).toBe(false);
- expect(findMonitorSettings().findComponent(ProjectFeatureSetting).props('options')).toEqual(
- expectedAccessLevel,
- );
- });
- it('when monitorAccessLevel is for project members, it is also for everyone', () => {
- wrapper = mountComponent({
- glFeatures: { splitOperationsVisibilityPermissions: true },
- currentSettings: { monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS },
- });
+ it('shows Monitor toggle instead of Operations toggle', () => {
+ wrapper = mountComponent({});
- expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.EVERYONE);
- });
+ expect(findMonitorSettings().exists()).toBe(true);
+ expect(findMonitorSettings().findComponent(ProjectFeatureSetting).props('options')).toEqual(
+ expectedAccessLevel,
+ );
});
- describe('without feature flag', () => {
- it('shows Operations toggle instead of Monitor toggle', () => {
- wrapper = mountComponent({});
-
- expect(findMonitorSettings().exists()).toBe(false);
- expect(findOperationsSettings().exists()).toBe(true);
- expect(
- findOperationsSettings().findComponent(ProjectFeatureSetting).props('options'),
- ).toEqual(expectedAccessLevel);
+ it('when monitorAccessLevel is for project members, it is also for everyone', () => {
+ wrapper = mountComponent({
+ currentSettings: { monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS },
});
+
+ expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.EVERYONE);
});
});
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
index 982c81b9272..7c9aae13d25 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -3,13 +3,13 @@ import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import WikiContent from '~/pages/shared/wikis/components/wiki_content.vue';
-import { renderGFM } from '~/pages/shared/wikis/render_gfm_facade';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { handleLocationHash } from '~/lib/utils/common_utils';
-jest.mock('~/pages/shared/wikis/render_gfm_facade');
+jest.mock('~/behaviors/markdown/render_gfm');
jest.mock('~/lib/utils/common_utils');
describe('pages/shared/wikis/components/wiki_content', () => {
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 437d51e02ba..5ab2c9abe5d 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem } from '@gitlab/ui';
import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -31,12 +30,8 @@ describe('detailedMetric', () => {
const findExpandedBacktraceBtnAtIndex = (index) => findExpandBacktraceBtns().at(index);
const findDetailsLabel = () => wrapper.findByTestId('performance-bar-details-label');
const findSortOrderDropdown = () => wrapper.findByTestId('performance-bar-sort-order');
- const clickSortOrderDropdownItem = (sortOrder) =>
- findSortOrderDropdown()
- .findAllComponents(GlDropdownItem)
- .filter((item) => item.text() === sortOrderOptions[sortOrder])
- .at(0)
- .vm.$emit('click');
+ const selectSortOrder = (sortOrder) =>
+ findSortOrderDropdown().vm.$emit('select', sortOrderOptions[sortOrder].value);
const findEmptyDetailNotice = () => wrapper.findByTestId('performance-bar-empty-detail-notice');
const findAllDetailDurations = () =>
wrapper.findAllByTestId('performance-item-duration').wrappers.map((w) => w.text());
@@ -334,11 +329,11 @@ describe('detailedMetric', () => {
});
it('changes sortOrder on select', async () => {
- clickSortOrderDropdownItem(sortOrders.CHRONOLOGICAL);
+ selectSortOrder(sortOrders.CHRONOLOGICAL);
await nextTick();
expect(findAllDetailDurations()).toEqual(['23ms', '100ms', '75ms']);
- clickSortOrderDropdownItem(sortOrders.DURATION);
+ selectSortOrder(sortOrders.DURATION);
await nextTick();
expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
});
diff --git a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js
deleted file mode 100644
index 512b152f106..00000000000
--- a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js
+++ /dev/null
@@ -1,456 +0,0 @@
-import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import { redirectTo } from '~/lib/utils/url_utility';
-import LegacyPipelineNewForm from '~/pipeline_new/components/legacy_pipeline_new_form.vue';
-import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
-import {
- mockQueryParams,
- mockPostParams,
- mockProjectId,
- mockError,
- mockRefs,
- mockCreditCardValidationRequiredError,
-} from '../mock_data';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
-}));
-
-const projectRefsEndpoint = '/root/project/refs';
-const pipelinesPath = '/root/project/-/pipelines';
-const configVariablesPath = '/root/project/-/pipelines/config_variables';
-const newPipelinePostResponse = { id: 1 };
-const defaultBranch = 'main';
-
-describe('Pipeline New Form', () => {
- let wrapper;
- let mock;
- let dummySubmitEvent;
-
- const findForm = () => wrapper.findComponent(GlForm);
- const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
- const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
- const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
- const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
- const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]');
- const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
- const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
- const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
- const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
- const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
- const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
- const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
-
- const selectBranch = (branch) => {
- // Select a branch in the dropdown
- findRefsDropdown().vm.$emit('input', {
- shortName: branch,
- fullName: `refs/heads/${branch}`,
- });
- };
-
- const createComponent = (props = {}, method = shallowMount) => {
- wrapper = method(LegacyPipelineNewForm, {
- provide: {
- projectRefsEndpoint,
- },
- propsData: {
- projectId: mockProjectId,
- pipelinesPath,
- configVariablesPath,
- defaultBranch,
- refParam: defaultBranch,
- settingsLink: '',
- maxWarnings: 25,
- ...props,
- },
- });
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
- mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
-
- dummySubmitEvent = {
- preventDefault: jest.fn(),
- };
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
- mock.restore();
- });
-
- describe('Form', () => {
- beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
- await waitForPromises();
- });
-
- it('displays the correct values for the provided query params', async () => {
- expect(findDropdowns().at(0).props('text')).toBe('Variable');
- expect(findDropdowns().at(1).props('text')).toBe('File');
- expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
- expect(findVariableRows()).toHaveLength(3);
- });
-
- it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(0).element.value).toBe('test_var');
- expect(findValueInputs().at(0).element.value).toBe('test_var_val');
- });
-
- it('displays an empty variable for the user to fill out', async () => {
- expect(findKeyInputs().at(2).element.value).toBe('');
- expect(findValueInputs().at(2).element.value).toBe('');
- expect(findDropdowns().at(2).props('text')).toBe('Variable');
- });
-
- it('does not display remove icon for last row', () => {
- expect(findRemoveIcons()).toHaveLength(2);
- });
-
- it('removes ci variable row on remove icon button click', async () => {
- findRemoveIcons().at(1).trigger('click');
-
- await nextTick();
-
- expect(findVariableRows()).toHaveLength(2);
- });
-
- it('creates blank variable on input change event', async () => {
- const input = findKeyInputs().at(2);
- input.element.value = 'test_var_2';
- input.trigger('change');
-
- await nextTick();
-
- expect(findVariableRows()).toHaveLength(4);
- expect(findKeyInputs().at(3).element.value).toBe('');
- expect(findValueInputs().at(3).element.value).toBe('');
- });
- });
-
- describe('Pipeline creation', () => {
- beforeEach(async () => {
- mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
-
- await waitForPromises();
- });
-
- it('does not submit the native HTML form', async () => {
- createComponent();
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- expect(dummySubmitEvent.preventDefault).toHaveBeenCalled();
- });
-
- it('disables the submit button immediately after submitting', async () => {
- createComponent();
-
- expect(findSubmitButton().props('disabled')).toBe(false);
-
- findForm().vm.$emit('submit', dummySubmitEvent);
- await waitForPromises();
-
- expect(findSubmitButton().props('disabled')).toBe(true);
- });
-
- it('creates pipeline with full ref and variables', async () => {
- createComponent();
-
- findForm().vm.$emit('submit', dummySubmitEvent);
- await waitForPromises();
-
- expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
- });
-
- it('creates a pipeline with short ref and variables from the query params', async () => {
- createComponent(mockQueryParams);
-
- await waitForPromises();
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
-
- expect(getFormPostParams()).toEqual(mockPostParams);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
- });
- });
-
- describe('When the ref has been changed', () => {
- beforeEach(async () => {
- createComponent({}, mount);
-
- await waitForPromises();
- });
- it('variables persist between ref changes', async () => {
- selectBranch('main');
-
- await waitForPromises();
-
- const mainInput = findKeyInputs().at(0);
- mainInput.element.value = 'build_var';
- mainInput.trigger('change');
-
- await nextTick();
-
- selectBranch('branch-1');
-
- await waitForPromises();
-
- const branchOneInput = findKeyInputs().at(0);
- branchOneInput.element.value = 'deploy_var';
- branchOneInput.trigger('change');
-
- await nextTick();
-
- selectBranch('main');
-
- await waitForPromises();
-
- expect(findKeyInputs().at(0).element.value).toBe('build_var');
- expect(findVariableRows().length).toBe(2);
-
- selectBranch('branch-1');
-
- await waitForPromises();
-
- expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
- expect(findVariableRows().length).toBe(2);
- });
- });
-
- describe('when yml defines a variable', () => {
- const mockYmlKey = 'yml_var';
- const mockYmlValue = 'yml_var_val';
- const mockYmlMultiLineValue = `A value
- with multiple
- lines`;
- const mockYmlDesc = 'A var from yml.';
-
- it('loading icon is shown when content is requested and hidden when received', async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
-
- expect(findLoadingIcon().exists()).toBe(true);
-
- await waitForPromises();
-
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('multi-line strings are added to the value field without removing line breaks', async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlMultiLineValue,
- description: mockYmlDesc,
- },
- });
-
- await waitForPromises();
-
- expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
- });
-
- describe('with description', () => {
- beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
-
- await waitForPromises();
- });
-
- it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(4);
- });
-
- it('displays a variable from yml', () => {
- expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
- expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
- });
-
- it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(1).element.value).toBe('test_var');
- expect(findValueInputs().at(1).element.value).toBe('test_var_val');
- });
-
- it('adds a description to the first variable from yml', () => {
- expect(findVariableRows().at(0).text()).toContain(mockYmlDesc);
- });
-
- it('removes the description when a variable key changes', async () => {
- findKeyInputs().at(0).element.value = 'yml_var_modified';
- findKeyInputs().at(0).trigger('change');
-
- await nextTick();
-
- expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
- });
- });
-
- describe('without description', () => {
- beforeEach(async () => {
- createComponent(mockQueryParams, mount);
-
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: null,
- },
- yml_var2: {
- value: 'yml_var2_val',
- },
- yml_var3: {
- description: '',
- },
- });
-
- await waitForPromises();
- });
-
- it('displays all the variables', async () => {
- expect(findVariableRows()).toHaveLength(3);
- });
- });
- });
-
- describe('Form errors and warnings', () => {
- beforeEach(() => {
- createComponent();
- });
-
- describe('when the refs cannot be loaded', () => {
- beforeEach(() => {
- mock
- .onGet(projectRefsEndpoint, { params: { search: '' } })
- .reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
-
- findRefsDropdown().vm.$emit('loadingError');
- });
-
- it('shows both an error alert', () => {
- expect(findErrorAlert().exists()).toBe(true);
- expect(findWarningAlert().exists()).toBe(false);
- });
- });
-
- describe('when the error response can be handled', () => {
- beforeEach(async () => {
- mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
- });
-
- it('shows both error and warning', () => {
- expect(findErrorAlert().exists()).toBe(true);
- expect(findWarningAlert().exists()).toBe(true);
- });
-
- it('shows the correct error', () => {
- expect(findErrorAlert().text()).toBe(mockError.errors[0]);
- });
-
- it('shows the correct warning title', () => {
- const { length } = mockError.warnings;
-
- expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
- });
-
- it('shows the correct amount of warnings', () => {
- expect(findWarnings()).toHaveLength(mockError.warnings.length);
- });
-
- it('re-enables the submit button', () => {
- expect(findSubmitButton().props('disabled')).toBe(false);
- });
-
- it('does not show the credit card validation required alert', () => {
- expect(findCCAlert().exists()).toBe(false);
- });
-
- describe('when the error response is credit card validation required', () => {
- beforeEach(async () => {
- mock
- .onPost(pipelinesPath)
- .reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError);
-
- window.gon = {
- subscriptions_url: TEST_HOST,
- payment_form_url: TEST_HOST,
- };
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
- });
-
- it('shows credit card validation required alert', () => {
- expect(findErrorAlert().exists()).toBe(false);
- expect(findCCAlert().exists()).toBe(true);
- });
-
- it('clears error and hides the alert on dismiss', async () => {
- expect(findCCAlert().exists()).toBe(true);
- expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]);
-
- findCCAlert().vm.$emit('dismiss');
-
- await nextTick();
-
- expect(findCCAlert().exists()).toBe(false);
- expect(wrapper.vm.$data.error).toBe(null);
- });
- });
- });
-
- describe('when the error response cannot be handled', () => {
- beforeEach(async () => {
- mock
- .onPost(pipelinesPath)
- .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
-
- findForm().vm.$emit('submit', dummySubmitEvent);
-
- await waitForPromises();
- });
-
- it('re-enables the submit button', () => {
- expect(findSubmitButton().props('disabled')).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 3e699b93fd3..2360dd7d103 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -295,11 +295,11 @@ describe('Pipeline New Form', () => {
expect(dropdownItems.at(2).text()).toBe(valueOptions[2]);
});
- it('variables with multiple predefined values sets the first option as the default', () => {
+ it('variable with multiple predefined values sets value as the default', () => {
const dropdown = findValueDropdowns().at(0);
const { valueOptions } = mockYamlVariables[2];
- expect(dropdown.props('text')).toBe(valueOptions[0]);
+ expect(dropdown.props('text')).toBe(valueOptions[1]);
});
});
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js
index e95a65171fc..2af0ef4d7c4 100644
--- a/spec/frontend/pipeline_new/mock_data.js
+++ b/spec/frontend/pipeline_new/mock_data.js
@@ -83,7 +83,7 @@ export const mockYamlVariables = [
{
description: 'This is a variable with predefined values.',
key: 'VAR_WITH_OPTIONS',
- value: 'development',
+ value: 'staging',
valueOptions: ['development', 'staging', 'production'],
},
];
@@ -105,7 +105,7 @@ export const mockYamlVariablesWithoutDesc = [
{
description: null,
key: 'VAR_WITH_OPTIONS',
- value: 'development',
+ value: 'staging',
valueOptions: ['development', 'staging', 'production'],
},
];
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
index 7fa8a18ea1f..036b82530d5 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
@@ -48,7 +48,6 @@ describe('Pipeline Mini Graph', () => {
isMergeTrain: false,
pipelinePath: '',
stages: expect.any(Array),
- stagesClass: '',
updateDropdown: false,
upstreamPipeline: undefined,
});
@@ -63,15 +62,6 @@ describe('Pipeline Mini Graph', () => {
expect(findUpstreamArrowIcon().exists()).toBe(false);
expect(findDownstreamArrowIcon().exists()).toBe(false);
});
-
- it('triggers events in "action request complete"', () => {
- createComponent();
-
- findPipelineMiniGraph(0).vm.$emit('pipelineActionRequestComplete');
- findPipelineMiniGraph(1).vm.$emit('pipelineActionRequestComplete');
-
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
- });
});
describe('rendered state with upstream pipeline', () => {
@@ -92,7 +82,6 @@ describe('Pipeline Mini Graph', () => {
isMergeTrain: false,
pipelinePath: '',
stages: expect.any(Array),
- stagesClass: '',
updateDropdown: false,
upstreamPipeline: expect.any(Object),
});
@@ -124,7 +113,6 @@ describe('Pipeline Mini Graph', () => {
isMergeTrain: false,
pipelinePath: 'my/pipeline/path',
stages: expect.any(Array),
- stagesClass: '',
updateDropdown: false,
upstreamPipeline: undefined,
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index 52b440f18bb..b7a9297d856 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -186,7 +186,7 @@ describe('Pipelines stage component', () => {
});
});
- describe('pipelineActionRequestComplete', () => {
+ describe('job update in dropdown', () => {
beforeEach(async () => {
mock.onGet(dropdownPath).reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
@@ -204,24 +204,11 @@ describe('Pipelines stage component', () => {
await findCiActionBtn().trigger('click');
};
- it('closes dropdown when job item action is clicked', async () => {
- const hidden = jest.fn();
-
- wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
-
- expect(hidden).toHaveBeenCalledTimes(0);
-
- await clickCiAction();
- await waitForPromises();
-
- expect(hidden).toHaveBeenCalledTimes(1);
- });
-
- it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => {
+ it('keeps dropdown open when job item action is clicked', async () => {
await clickCiAction();
await waitForPromises();
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1);
+ expect(findDropdown().classes('show')).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
index bfb780d5d39..c123f53886e 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
@@ -26,12 +26,6 @@ describe('Pipeline Stages', () => {
expect(findPipelineStages()).toHaveLength(mockStages.length);
});
- it('renders stages with a custom class', () => {
- createComponent({ stagesClass: 'my-class' });
-
- expect(wrapper.findAll('.my-class')).toHaveLength(mockStages.length);
- });
-
it('does not fail when stages are empty', () => {
createComponent({ stages: [] });
@@ -39,15 +33,6 @@ describe('Pipeline Stages', () => {
expect(findPipelineStages()).toHaveLength(0);
});
- it('triggers events in "action request complete" in stages', () => {
- createComponent();
-
- findPipelineStagesAt(0).vm.$emit('pipelineActionRequestComplete');
- findPipelineStagesAt(1).vm.$emit('pipelineActionRequestComplete');
-
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
- });
-
it('update dropdown is false by default', () => {
createComponent();
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index ee3eaaf5ef3..ba7262353f0 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -6,7 +6,10 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
import { users, mockSearch, branches, tags } from '../mock_data';
@@ -63,7 +66,7 @@ describe('Pipelines filtered search', () => {
title: 'Trigger author',
unique: true,
projectId: '21',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findBranchToken()).toMatchObject({
@@ -73,7 +76,7 @@ describe('Pipelines filtered search', () => {
unique: true,
projectId: '21',
defaultBranchName: 'main',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findSourceToken()).toMatchObject({
@@ -81,7 +84,7 @@ describe('Pipelines filtered search', () => {
icon: 'trigger-source',
title: 'Source',
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findStatusToken()).toMatchObject({
@@ -89,7 +92,7 @@ describe('Pipelines filtered search', () => {
icon: 'status',
title: 'Status',
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
expect(findTagToken()).toMatchObject({
@@ -97,7 +100,7 @@ describe('Pipelines filtered search', () => {
icon: 'tag',
title: 'Tag name',
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
});
});
@@ -111,7 +114,7 @@ describe('Pipelines filtered search', () => {
it('disables tag name token when branch name token is active', async () => {
findFilteredSearch().vm.$emit('input', [
{ type: 'ref', value: { data: 'branch-1', operator: '=' } },
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
]);
await nextTick();
@@ -122,7 +125,7 @@ describe('Pipelines filtered search', () => {
it('disables branch name token when tag name token is active', async () => {
findFilteredSearch().vm.$emit('input', [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
]);
await nextTick();
@@ -139,7 +142,7 @@ describe('Pipelines filtered search', () => {
});
it('resets tokens disabled state when clearing tokens by backspace', async () => {
- findFilteredSearch().vm.$emit('input', [{ type: 'filtered-search-term', value: { data: '' } }]);
+ findFilteredSearch().vm.$emit('input', [{ type: FILTERED_SEARCH_TERM, value: { data: '' } }]);
await nextTick();
expect(findBranchToken().disabled).toBe(false);
@@ -172,7 +175,7 @@ describe('Pipelines filtered search', () => {
operator: '=',
},
},
- { type: 'filtered-search-term', value: { data: '' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: '' } },
];
expect(findFilteredSearch().props('value')).toMatchObject(expectedValueProp);
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
index b537c81da3f..f255e0d857f 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
@@ -13,7 +13,7 @@ import {
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
I18N,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
const ciRunnerSettingsPath = '/-/settings/ci_cd';
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index d9199f3b0f7..df10742fd93 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,7 +1,7 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
-import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
+import { CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 044683ce533..740037a5ac8 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -17,7 +17,6 @@ import {
TRACKING_CATEGORIES,
} from '~/pipelines/constants';
-import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
jest.mock('~/pipelines/event_hub');
@@ -134,12 +133,6 @@ describe('Pipelines Table', () => {
expect(findPipelineMiniGraph().props('stages')).toHaveLength(0);
});
});
-
- it('when action request is complete, should refresh table', () => {
- findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
- });
});
describe('duration cell', () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
index 94f9a37f707..c090fd353f7 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -2,6 +2,10 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitl
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue';
+import {
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
describe('Pipeline Status Token', () => {
let wrapper;
@@ -13,9 +17,9 @@ describe('Pipeline Status Token', () => {
const defaultProps = {
config: {
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: 'Status',
+ title: TOKEN_TITLE_STATUS,
unique: true,
},
value: {
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index eba6b95214d..1299e7277d1 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -57,12 +57,13 @@ describe('popovers/components/popovers.vue', () => {
describe('supports HTML content', () => {
const svgIcon = '<svg><use xlink:href="icons.svg#test"></use></svg>';
+ const escapedSvgIcon = '<svg><use xlink:href=&quot;icons.svg#test&quot;></use></svg>';
it.each`
description | content | render
${'renders html content correctly'} | ${'<b>HTML</b>'} | ${'<b>HTML</b>'}
${'removes any unsafe content'} | ${'<script>alert(XSS)</script>'} | ${''}
- ${'renders svg icons correctly'} | ${svgIcon} | ${svgIcon}
+ ${'renders svg icons correctly'} | ${svgIcon} | ${escapedSvgIcon}
`('$description', async ({ content, render }) => {
await buildWrapper(createPopoverTarget({ content, html: true }));
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index e2848e615c3..a84dd246f5d 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -13,7 +13,7 @@ describe('BranchesDropdown', () => {
let store;
const spyFetchBranches = jest.fn();
- const createComponent = (term, state = { isFetching: false }) => {
+ const createComponent = (props, state = { isFetching: false }) => {
store = new Vuex.Store({
getters: {
joinedBranches: () => ['_main_', '_branch_1_', '_branch_2_'],
@@ -28,7 +28,8 @@ describe('BranchesDropdown', () => {
shallowMount(BranchesDropdown, {
store,
propsData: {
- value: term,
+ value: props.value,
+ blanked: props.blanked || false,
},
}),
);
@@ -48,23 +49,40 @@ describe('BranchesDropdown', () => {
describe('On mount', () => {
beforeEach(() => {
- createComponent('');
+ createComponent({ value: '' });
});
it('invokes fetchBranches', () => {
expect(spyFetchBranches).toHaveBeenCalled();
});
+
+ describe('with a value but visually blanked', () => {
+ beforeEach(() => {
+ createComponent({ value: '_main_', blanked: true }, { branch: '_main_' });
+ });
+
+ it('renders all branches', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ expect(findDropdownItemByIndex(0).text()).toBe('_main_');
+ expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
+ expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
+ });
+
+ it('selects the active branch', () => {
+ expect(wrapper.vm.isSelected('_main_')).toBe(true);
+ });
+ });
});
describe('Loading states', () => {
it('shows loading icon while fetching', () => {
- createComponent('', { isFetching: true });
+ createComponent({ value: '' }, { isFetching: true });
expect(findLoading().isVisible()).toBe(true);
});
it('does not show loading icon', () => {
- createComponent('');
+ createComponent({ value: '' });
expect(findLoading().isVisible()).toBe(false);
});
@@ -72,7 +90,7 @@ describe('BranchesDropdown', () => {
describe('No branches found', () => {
beforeEach(() => {
- createComponent('_non_existent_branch_');
+ createComponent({ value: '_non_existent_branch_' });
});
it('renders empty results message', () => {
@@ -90,7 +108,7 @@ describe('BranchesDropdown', () => {
describe('Search term is empty', () => {
beforeEach(() => {
- createComponent('');
+ createComponent({ value: '' });
});
it('renders all branches when search term is empty', () => {
@@ -107,7 +125,7 @@ describe('BranchesDropdown', () => {
describe('When searching', () => {
beforeEach(() => {
- createComponent('');
+ createComponent({ value: '' });
});
it('invokes fetchBranches', async () => {
@@ -124,7 +142,7 @@ describe('BranchesDropdown', () => {
describe('Branches found', () => {
beforeEach(() => {
- createComponent('_branch_1_', { branch: '_branch_1_' });
+ createComponent({ value: '_branch_1_' }, { branch: '_branch_1_' });
});
it('renders only the branch searched for', () => {
@@ -156,7 +174,7 @@ describe('BranchesDropdown', () => {
describe('Case insensitive for search term', () => {
beforeEach(() => {
- createComponent('_BrAnCh_1_');
+ createComponent({ value: '_BrAnCh_1_' });
});
it('renders only the branch searched for', () => {
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index b6d4ee32cf5..67532cea61e 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -63,6 +63,8 @@ describe('NewProjectUrlSelect component', () => {
rootUrl: 'https://gitlab.com/',
trackLabel: 'blank_project',
userNamespaceId: '1',
+ inputId: 'input_id',
+ inputName: 'input_name',
};
let mockQueryResponse;
@@ -92,7 +94,7 @@ describe('NewProjectUrlSelect component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findSelectedPath = () => wrapper.findComponent(GlTruncate);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
- const findHiddenNamespaceInput = () => wrapper.find('[name="project[namespace_id]"]');
+ const findHiddenNamespaceInput = () => wrapper.find(`[name="${defaultProvide.inputName}`);
const findHiddenSelectedNamespaceInput = () =>
wrapper.find('[name="project[selected_namespace_id]"]');
@@ -165,6 +167,8 @@ describe('NewProjectUrlSelect component', () => {
it("renders a hidden input with the user's namespace id", () => {
expect(findHiddenNamespaceInput().attributes('value')).toBe(defaultProvide.userNamespaceId);
+ expect(findHiddenNamespaceInput().attributes('name')).toBe(defaultProvide.inputName);
+ expect(findHiddenNamespaceInput().attributes('id')).toBe(defaultProvide.inputId);
});
it('renders a hidden input with the selected namespace id', () => {
@@ -198,6 +202,18 @@ describe('NewProjectUrlSelect component', () => {
expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath);
});
+ it('does not render users section when user namespace id is not provided', async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ provide: { ...defaultProvide, userNamespaceId: null },
+ });
+
+ await showDropdown();
+
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader)).toHaveLength(1);
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(0).text()).toBe('Groups');
+ });
+
describe('query fetching', () => {
describe('on component mount', () => {
it('does not fetch query', () => {
@@ -297,7 +313,7 @@ describe('NewProjectUrlSelect component', () => {
);
});
- it('tracks clicking on the dropdown', () => {
+ it('tracks clicking on the dropdown when trackLabel is provided', () => {
wrapper = mountComponent();
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
@@ -311,4 +327,16 @@ describe('NewProjectUrlSelect component', () => {
unmockTracking();
});
+
+ it('does not track clicking on the dropdown when trackLabel is not provided', () => {
+ wrapper = mountComponent({ provide: { ...defaultProvide, trackLabel: null } });
+
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findDropdown().vm.$emit('show');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+
+ unmockTracking();
+ });
});
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index 4fcecc3a307..d69bfc4ec92 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -1,12 +1,14 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import projectNew from '~/projects/project_new';
+import { checkRules } from '~/projects/project_name_rules';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
describe('New Project', () => {
let $projectImportUrl;
let $projectPath;
let $projectName;
+ let $projectNameError;
const mockKeyup = (el) => el.dispatchEvent(new KeyboardEvent('keyup'));
const mockChange = (el) => el.dispatchEvent(new Event('change'));
@@ -29,6 +31,7 @@ describe('New Project', () => {
</div>
</div>
<input id="project_name" />
+ <div class="gl-field-error hidden" id="project_name_error" />
<input id="project_path" />
</div>
<div class="js-user-readme-repo"></div>
@@ -41,6 +44,7 @@ describe('New Project', () => {
$projectImportUrl = document.querySelector('#project_import_url');
$projectPath = document.querySelector('#project_path');
$projectName = document.querySelector('#project_name');
+ $projectNameError = document.querySelector('#project_name_error');
});
afterEach(() => {
@@ -84,6 +88,57 @@ describe('New Project', () => {
});
});
+ describe('tracks manual name input', () => {
+ beforeEach(() => {
+ projectNew.bindEvents();
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('no error message by default', () => {
+ expect($projectNameError.classList.contains('hidden')).toBe(true);
+ });
+
+ it('show error message if name is validate', () => {
+ $projectName.value = '.validate!Name';
+ triggerEvent($projectName, 'change');
+
+ expect($projectNameError.innerText).toBe(
+ "Name must start with a letter, digit, emoji, or '_'",
+ );
+ expect($projectNameError.classList.contains('hidden')).toBe(false);
+ });
+ });
+
+ describe('project name rule', () => {
+ describe("Name must start with a letter, digit, emoji, or '_'", () => {
+ const errormsg = "Name must start with a letter, digit, emoji, or '_'";
+ it("'.foo' should error", () => {
+ const text = '.foo';
+ expect(checkRules(text)).toBe(errormsg);
+ });
+ it('_foo should passed', () => {
+ const text = '_foo';
+ expect(checkRules(text)).toBe('');
+ });
+ });
+
+ describe("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces", () => {
+ const errormsg =
+ "Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces";
+ it("'foo(#^.^#)foo' should error", () => {
+ const text = 'foo(#^.^#)foo';
+ expect(checkRules(text)).toBe(errormsg);
+ });
+ it("'foo123😊_.+- ' should passed", () => {
+ const text = 'foo123😊_.+- ';
+ expect(checkRules(text)).toBe('');
+ });
+ });
+ });
+
describe('deriveProjectPathFromUrl', () => {
const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`;
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index 27065a704e2..bc373d9deb7 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -16,10 +16,12 @@ import {
branchProtectionsMockResponse,
approvalRulesMock,
statusChecksRulesMock,
+ matchingBranchesCount,
} from './mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
+ mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=main'),
joinPaths: jest.fn(),
}));
@@ -65,6 +67,13 @@ describe('View branch rules', () => {
const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription);
const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
+ const findMatchingBranchesLink = () =>
+ wrapper.findByText(
+ sprintf(I18N.matchingBranchesLinkTitle, {
+ total: matchingBranchesCount,
+ subject: 'branches',
+ }),
+ );
it('gets the branch param from url and renders it in the view', () => {
expect(util.getParameterByName).toHaveBeenCalledWith('branch');
@@ -85,6 +94,12 @@ describe('View branch rules', () => {
expect(findBranchTitle().exists()).toBe(true);
});
+ it('renders matching branches link', () => {
+ const matchingBranchesLink = findMatchingBranchesLink();
+ expect(matchingBranchesLink.exists()).toBe(true);
+ expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=main');
+ });
+
it('renders a branch protection title', () => {
expect(findBranchProtectionTitle().exists()).toBe(true);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
index c07d4673344..821dba75b62 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
@@ -109,6 +109,8 @@ export const accessLevelsMockResponse = [
},
];
+export const matchingBranchesCount = 3;
+
export const branchProtectionsMockResponse = {
data: {
project: {
@@ -141,6 +143,7 @@ export const branchProtectionsMockResponse = {
__typename: 'ExternalStatusCheckConnection',
nodes: statusChecksRulesMock,
},
+ matchingBranchesCount,
},
{
__typename: 'BranchRule',
@@ -166,6 +169,7 @@ export const branchProtectionsMockResponse = {
__typename: 'ExternalStatusCheckConnection',
nodes: [],
},
+ matchingBranchesCount,
},
],
},
diff --git a/spec/frontend/projects/settings/mock_data.js b/spec/frontend/projects/settings/mock_data.js
new file mode 100644
index 00000000000..0262c0e3e43
--- /dev/null
+++ b/spec/frontend/projects/settings/mock_data.js
@@ -0,0 +1,57 @@
+const accessLevelsMockResponse = [
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Jona Langworth',
+ group: null,
+ user: {
+ __typename: 'UserCore',
+ id: '123',
+ webUrl: 'test.com',
+ name: 'peter',
+ avatarUrl: 'test.com/user.png',
+ },
+ },
+ },
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Maintainers',
+ group: null,
+ user: null,
+ },
+ },
+];
+
+export const pushAccessLevelsMockResponse = {
+ __typename: 'PushAccessLevelConnection',
+ edges: accessLevelsMockResponse,
+};
+
+export const pushAccessLevelsMockResult = {
+ total: 2,
+ users: [
+ {
+ src: 'test.com/user.png',
+ __typename: 'UserCore',
+ id: '123',
+ webUrl: 'test.com',
+ name: 'peter',
+ avatarUrl: 'test.com/user.png',
+ },
+ ],
+ groups: [],
+ roles: [
+ {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Maintainers',
+ group: null,
+ user: null,
+ },
+ ],
+};
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index 6369f04781f..447d7e86ceb 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -5,9 +5,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
-import branchRulesQuery from '~/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
+import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { createAlert } from '~/flash';
-import { branchRulesMockResponse, appProvideMock } from './mock_data';
+import {
+ branchRulesMockResponse,
+ appProvideMock,
+} from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index 2aa93fd0e28..49c45c080b4 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -50,17 +50,15 @@ describe('Branch rule', () => {
it('renders the protection details list items', () => {
expect(findProtectionDetailsListItems()).toHaveLength(wrapper.vm.approvalDetails.length);
expect(findProtectionDetailsListItems().at(0).text()).toBe(i18n.allowForcePush);
- expect(findProtectionDetailsListItems().at(1).text()).toBe(i18n.codeOwnerApprovalRequired);
- expect(findProtectionDetailsListItems().at(2).text()).toMatchInterpolatedText(
- sprintf(i18n.statusChecks, {
- total: branchRulePropsMock.statusChecksTotal,
- subject: n__('check', 'checks', branchRulePropsMock.statusChecksTotal),
- }),
- );
- expect(findProtectionDetailsListItems().at(3).text()).toMatchInterpolatedText(
- sprintf(i18n.approvalRules, {
- total: branchRulePropsMock.approvalRulesTotal,
- subject: n__('rule', 'rules', branchRulePropsMock.approvalRulesTotal),
+ expect(findProtectionDetailsListItems().at(1).text()).toBe(wrapper.vm.pushAccessLevelsText);
+ });
+
+ it('renders branches count for wildcards', () => {
+ createComponent({ name: 'test-*' });
+ expect(findProtectionDetailsListItems().at(0).text()).toMatchInterpolatedText(
+ sprintf(i18n.matchingBranches, {
+ total: branchRulePropsMock.matchingBranchesCount,
+ subject: n__('branch', 'branches', branchRulePropsMock.matchingBranchesCount),
}),
);
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index 8aa03a12996..6f506882c36 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -1,3 +1,22 @@
+export const accessLevelsMockResponse = [
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Developers',
+ },
+ },
+ {
+ __typename: 'PushAccessLevelEdge',
+ node: {
+ __typename: 'PushAccessLevel',
+ accessLevel: 40,
+ accessLevelDescription: 'Maintainers',
+ },
+ },
+];
+
export const branchRulesMockResponse = {
data: {
project: {
@@ -9,34 +28,34 @@ export const branchRulesMockResponse = {
{
name: 'main',
isDefault: true,
+ matchingBranchesCount: 1,
branchProtection: {
allowForcePush: true,
- codeOwnerApprovalRequired: true,
- },
- approvalRules: {
- nodes: [{ id: 1 }],
- __typename: 'ApprovalProjectRuleConnection',
- },
- externalStatusChecks: {
- nodes: [{ id: 1 }, { id: 2 }],
- __typename: 'BranchRule',
+ mergeAccessLevels: {
+ edges: [],
+ __typename: 'MergeAccessLevelConnection',
+ },
+ pushAccessLevels: {
+ edges: accessLevelsMockResponse,
+ __typename: 'PushAccessLevelConnection',
+ },
},
__typename: 'BranchRule',
},
{
name: 'test-*',
isDefault: false,
+ matchingBranchesCount: 2,
branchProtection: {
allowForcePush: false,
- codeOwnerApprovalRequired: false,
- },
- approvalRules: {
- nodes: [],
- __typename: 'ApprovalProjectRuleConnection',
- },
- externalStatusChecks: {
- nodes: [],
- __typename: 'BranchRule',
+ mergeAccessLevels: {
+ edges: [],
+ __typename: 'MergeAccessLevelConnection',
+ },
+ pushAccessLevels: {
+ edges: [],
+ __typename: 'PushAccessLevelConnection',
+ },
},
__typename: 'BranchRule',
},
@@ -57,17 +76,22 @@ export const branchRuleProvideMock = {
export const branchRulePropsMock = {
name: 'main',
isDefault: true,
+ matchingBranchesCount: 1,
branchProtection: {
allowForcePush: true,
- codeOwnerApprovalRequired: true,
+ codeOwnerApprovalRequired: false,
+ pushAccessLevels: {
+ edges: accessLevelsMockResponse,
+ },
},
- approvalRulesTotal: 1,
- statusChecksTotal: 2,
+ approvalRulesTotal: 0,
+ statusChecksTotal: 0,
};
export const branchRuleWithoutDetailsPropsMock = {
- name: 'main',
+ name: 'branch-1',
isDefault: false,
+ matchingBranchesCount: 1,
branchProtection: {
allowForcePush: false,
codeOwnerApprovalRequired: false,
diff --git a/spec/frontend/projects/settings/utils_spec.js b/spec/frontend/projects/settings/utils_spec.js
new file mode 100644
index 00000000000..319aa4000b5
--- /dev/null
+++ b/spec/frontend/projects/settings/utils_spec.js
@@ -0,0 +1,11 @@
+import { getAccessLevels } from '~/projects/settings/utils';
+import { pushAccessLevelsMockResponse, pushAccessLevelsMockResult } from './mock_data';
+
+describe('Utils', () => {
+ describe('getAccessLevels', () => {
+ it('takes accessLevels response data and returns acecssLevels object', () => {
+ const pushAccessLevels = getAccessLevels(pushAccessLevelsMockResponse);
+ expect(pushAccessLevels).toEqual(pushAccessLevelsMockResult);
+ });
+ });
+});
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index d88d79d2cde..00fc521b716 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -43,16 +43,17 @@ Object {
},
"author": Object {
"__typename": "UserCore",
- "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
+ "avatarUrl": "https://www.gravatar.com/avatar/eb329fbfeccd9e6d45ff159da8736876?s=80&d=identicon",
"id": Any<String>,
- "username": "administrator",
- "webUrl": "http://localhost/administrator",
+ "username": "user1",
+ "webUrl": "http://localhost/user1",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "createdAt": 2019-01-03T00:00:00.000Z,
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>",
"evidences": Array [],
"historicalRelease": false,
@@ -140,16 +141,17 @@ Object {
},
"author": Object {
"__typename": "UserCore",
- "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
+ "avatarUrl": "https://www.gravatar.com/avatar/eb329fbfeccd9e6d45ff159da8736876?s=80&d=identicon",
"id": Any<String>,
- "username": "administrator",
- "webUrl": "http://localhost/administrator",
+ "username": "user1",
+ "webUrl": "http://localhost/user1",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "createdAt": 2018-12-03T00:00:00.000Z,
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
"evidences": Array [
Object {
@@ -253,6 +255,7 @@ Object {
"sources": Array [],
},
"author": undefined,
+ "createdAt": 2018-12-03T00:00:00.000Z,
"description": "Best. Release. **Ever.** :rocket:",
"evidences": Array [],
"milestones": Array [
@@ -362,16 +365,17 @@ Object {
},
"author": Object {
"__typename": "UserCore",
- "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
+ "avatarUrl": "https://www.gravatar.com/avatar/eb329fbfeccd9e6d45ff159da8736876?s=80&d=identicon",
"id": Any<String>,
- "username": "administrator",
- "webUrl": "http://localhost/administrator",
+ "username": "user1",
+ "webUrl": "http://localhost/user1",
},
"commit": Object {
"shortId": "b83d6e39",
"title": "Merge branch 'branch-merged' into 'master'",
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "createdAt": 2018-12-03T00:00:00.000Z,
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
"evidences": Array [
Object {
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index 8f4efad197f..19b41d05a44 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash';
import { nextTick } from 'vue';
import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
+import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
import { trimText } from 'helpers/text_helper';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
@@ -43,88 +44,118 @@ describe('Release block footer', () => {
const tagInfoSectionLink = () => tagInfoSection().findComponent(GlLink);
const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
- describe('with all props provided', () => {
- beforeEach(() => factory());
-
- it('renders the commit icon', () => {
- const commitIcon = commitInfoSection().findComponent(GlIcon);
-
- expect(commitIcon.exists()).toBe(true);
- expect(commitIcon.props('name')).toBe('commit');
- });
-
- it('renders the commit SHA with a link', () => {
- const commitLink = commitInfoSectionLink();
-
- expect(commitLink.exists()).toBe(true);
- expect(commitLink.text()).toBe(release.commit.shortId);
- expect(commitLink.attributes('href')).toBe(release.commitPath);
- });
-
- it('renders the tag icon', () => {
- const commitIcon = tagInfoSection().findComponent(GlIcon);
-
- expect(commitIcon.exists()).toBe(true);
- expect(commitIcon.props('name')).toBe('tag');
- });
-
- it('renders the tag name with a link', () => {
- const commitLink = tagInfoSection().findComponent(GlLink);
-
- expect(commitLink.exists()).toBe(true);
- expect(commitLink.text()).toBe(release.tagName);
- expect(commitLink.attributes('href')).toBe(release.tagPath);
- });
-
- it('renders the author and creation time info', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(
- `Created 1 year ago by ${release.author.username}`,
- );
- });
-
- describe('when the release date is in the past', () => {
- it('prefixes the creation info with "Created"', () => {
- expect(trimText(authorDateInfoSection().text())).toEqual(expect.stringMatching(/^Created/));
- });
- });
-
- describe('renders the author and creation time info with future release date', () => {
- beforeEach(() => {
- factory({ releasedAt: mockFutureDate });
- });
-
- it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(
- `Will be created in 1 month by ${release.author.username}`,
- );
- });
- });
-
- describe('when the release date is in the future', () => {
- beforeEach(() => {
- factory({ releasedAt: mockFutureDate });
- });
-
- it('prefixes the creation info with "Will be created"', () => {
- expect(trimText(authorDateInfoSection().text())).toEqual(
- expect.stringMatching(/^Will be created/),
- );
- });
- });
-
- it("renders the author's avatar image", () => {
- const avatarImg = authorDateInfoSection().find('img');
-
- expect(avatarImg.exists()).toBe(true);
- expect(avatarImg.attributes('src')).toBe(release.author.avatarUrl);
- });
-
- it("renders a link to the author's profile", () => {
- const authorLink = authorDateInfoSection().findComponent(GlLink);
-
- expect(authorLink.exists()).toBe(true);
- expect(authorLink.attributes('href')).toBe(release.author.webUrl);
- });
+ describe.each`
+ sortFlag | expectedInfoString
+ ${null} | ${'Created'}
+ ${CREATED_ASC} | ${'Created'}
+ ${CREATED_DESC} | ${'Created'}
+ ${RELEASED_AT_ASC} | ${'Released'}
+ ${RELEASED_AT_DESC} | ${'Released'}
+ `('with sorting set to $sortFlag', ({ sortFlag, expectedInfoString }) => {
+ const dateAt =
+ expectedInfoString === 'Created' ? originalRelease.createdAt : originalRelease.releasedAt;
+
+ describe.each`
+ dateType | dateFlag | expectedInfoStringPrefix | expectedDateString
+ ${'empty'} | ${undefined} | ${null} | ${null}
+ ${'in the past'} | ${dateAt} | ${null} | ${'1 year ago'}
+ ${'in the future'} | ${mockFutureDate} | ${'Will be'} | ${'in 1 month'}
+ `(
+ 'with date set to $dateType',
+ ({ dateFlag, expectedInfoStringPrefix, expectedDateString }) => {
+ describe.each`
+ authorType | authorFlag | expectedAuthorString
+ ${'empty'} | ${undefined} | ${null}
+ ${'present'} | ${originalRelease.author} | ${'by user1'}
+ `('with author set to $authorType', ({ authorFlag, expectedAuthorString }) => {
+ const propsData = { sort: sortFlag, author: authorFlag };
+ if (dateFlag !== '') {
+ propsData.createdAt = dateFlag;
+ propsData.releasedAt = dateFlag;
+ }
+
+ beforeEach(() => {
+ factory({ ...propsData });
+ });
+
+ const expectedString = [
+ expectedInfoStringPrefix,
+ expectedInfoStringPrefix ? expectedInfoString.toLowerCase() : expectedInfoString,
+ expectedDateString,
+ expectedAuthorString,
+ ];
+
+ if (authorFlag || dateFlag) {
+ it('renders the author and creation time info', () => {
+ expect(trimText(authorDateInfoSection().text())).toBe(
+ expectedString.filter((n) => n).join(' '),
+ );
+ });
+ if (authorFlag) {
+ it("renders the author's avatar image", () => {
+ const avatarImg = authorDateInfoSection().find('img');
+
+ expect(avatarImg.exists()).toBe(true);
+ expect(avatarImg.attributes('src')).toBe(release.author.avatarUrl);
+ });
+
+ it("renders a link to the author's profile", () => {
+ const authorLink = authorDateInfoSection().findComponent(GlLink);
+
+ expect(authorLink.exists()).toBe(true);
+ expect(authorLink.attributes('href')).toBe(release.author.webUrl);
+ });
+ } else {
+ it("does not render the author's avatar image", () => {
+ const avatarImg = authorDateInfoSection().find('img');
+
+ expect(avatarImg.exists()).toBe(false);
+ });
+
+ it("does not render a link to the author's profile", () => {
+ const authorLink = authorDateInfoSection().findComponent(GlLink);
+
+ expect(authorLink.exists()).toBe(false);
+ });
+ }
+ } else {
+ it('does not render the author and creation time info', () => {
+ expect(authorDateInfoSection().exists()).toBe(false);
+ });
+ }
+
+ it('renders the commit icon', () => {
+ const commitIcon = commitInfoSection().findComponent(GlIcon);
+
+ expect(commitIcon.exists()).toBe(true);
+ expect(commitIcon.props('name')).toBe('commit');
+ });
+
+ it('renders the commit SHA with a link', () => {
+ const commitLink = commitInfoSectionLink();
+
+ expect(commitLink.exists()).toBe(true);
+ expect(commitLink.text()).toBe(release.commit.shortId);
+ expect(commitLink.attributes('href')).toBe(release.commitPath);
+ });
+
+ it('renders the tag icon', () => {
+ const commitIcon = tagInfoSection().findComponent(GlIcon);
+
+ expect(commitIcon.exists()).toBe(true);
+ expect(commitIcon.props('name')).toBe('tag');
+ });
+
+ it('renders the tag name with a link', () => {
+ const commitLink = tagInfoSection().findComponent(GlLink);
+
+ expect(commitLink.exists()).toBe(true);
+ expect(commitLink.text()).toBe(release.tagName);
+ expect(commitLink.attributes('href')).toBe(release.tagPath);
+ });
+ });
+ },
+ );
});
describe('without any commit info', () => {
@@ -160,40 +191,4 @@ describe('Release block footer', () => {
expect(tagInfoSection().text()).toBe(release.tagName);
});
});
-
- describe('without any author info', () => {
- beforeEach(() => factory({ author: undefined }));
-
- it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(`Created 1 year ago`);
- });
- });
-
- describe('future release without any author info', () => {
- beforeEach(() => {
- factory({ author: undefined, releasedAt: mockFutureDate });
- });
-
- it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(`Will be created in 1 month`);
- });
- });
-
- describe('without a released at date', () => {
- beforeEach(() => factory({ releasedAt: undefined }));
-
- it('renders the author name without the release date', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(
- `Created by ${release.author.username}`,
- );
- });
- });
-
- describe('without a release date or author info', () => {
- beforeEach(() => factory({ author: undefined, releasedAt: undefined }));
-
- it('does not render any author or release date info', () => {
- expect(authorDateInfoSection().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 096c3db8902..f1b8554fbc3 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,5 +1,4 @@
import { mount } from '@vue/test-utils';
-import $ from 'jquery';
import { nextTick } from 'vue';
import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
@@ -10,6 +9,9 @@ import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('Release block', () => {
let wrapper;
@@ -34,7 +36,6 @@ describe('Release block', () => {
const editButton = () => wrapper.find('.js-edit-button');
beforeEach(() => {
- jest.spyOn($.fn, 'renderGFM');
release = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data;
});
@@ -62,7 +63,7 @@ describe('Release block', () => {
it('renders release description', () => {
expect(wrapper.vm.$refs['gfm-content']).toBeDefined();
- expect($.fn.renderGFM).toHaveBeenCalledTimes(1);
+ expect(renderGFM).toHaveBeenCalledTimes(1);
});
it('renders release date', () => {
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 2180f78a8df..8b987551b33 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -82,9 +82,6 @@ function factory({ path, isLoading = false, hasMore = true, entries = {}, commit
mocks: {
$apollo,
},
- provide: {
- glFeatures: { lazyLoadCommits: true },
- },
});
}
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 64aa6d179a8..5d9138ab9cd 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -30,9 +30,6 @@ function factory(propsData = {}) {
directives: {
GlHoverLoad: createMockDirective(),
},
- provide: {
- glFeatures: { lazyLoadCommits: true },
- },
mocks: {
$router,
},
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 352f4314232..6eea66f1a7d 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -31,7 +31,6 @@ function factory(path, data = () => ({})) {
glFeatures: {
increasePageSizeExponentially: true,
paginatedTreeGraphqlQuery: true,
- lazyLoadCommits: true,
},
},
});
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
new file mode 100644
index 00000000000..3335059554f
--- /dev/null
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -0,0 +1,22 @@
+import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const projectRootPath = 'root/Project1';
+const currentRef = 'main';
+const selectedRef = 'feature';
+
+describe('generateRefDestinationPath', () => {
+ it.each`
+ currentPath | result
+ ${projectRootPath} | ${`${projectRootPath}/-/tree/${selectedRef}`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1`} | ${`${projectRootPath}/-/tree/${selectedRef}/dir1`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2`} | ${`${projectRootPath}/-/tree/${selectedRef}/dir1/dir2`}
+ ${`${projectRootPath}/-/blob/${currentRef}/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
+ `('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result);
+ });
+});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index fa5ccfeb478..e02d3b0eab8 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -114,6 +114,7 @@ export const MOCK_NAVIGATION = {
scope: 'projects',
link: '/search?scope=projects&search=et',
count_link: '/search/count?scope=projects&search=et',
+ count: '10,000+',
},
blobs: {
label: 'Code',
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index c57eabd57b9..d5ecca4636c 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -1,39 +1,16 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { MOCK_QUERY } from 'jest/search/mock_data';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
-Vue.use(Vuex);
-
describe('ConfidentialityFilter', () => {
let wrapper;
- const actionSpies = {
- applyQuery: jest.fn(),
- resetQuery: jest.fn(),
- };
-
- const createComponent = (initialState) => {
- const store = new Vuex.Store({
- state: {
- query: MOCK_QUERY,
- ...initialState,
- },
- actions: actionSpies,
- });
-
+ const createComponent = (initProps) => {
wrapper = shallowMount(ConfidentialityFilter, {
- store,
+ ...initProps,
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
describe('template', () => {
@@ -41,24 +18,28 @@ describe('ConfidentialityFilter', () => {
createComponent();
});
- describe.each`
- scope | showFilter
- ${'issues'} | ${true}
- ${'merge_requests'} | ${false}
- ${'projects'} | ${false}
- ${'milestones'} | ${false}
- ${'users'} | ${false}
- ${'notes'} | ${false}
- ${'wiki_blobs'} | ${false}
- ${'blobs'} | ${false}
- `(`dropdown`, ({ scope, showFilter }) => {
- beforeEach(() => {
- createComponent({ query: { scope } });
- });
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+ });
- it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
- expect(findRadioFilter().exists()).toBe(showFilter);
+ describe.each`
+ hasFeatureFlagEnabled | paddingClass
+ ${true} | ${'gl-px-5'}
+ ${false} | ${'gl-px-0'}
+ `(`RadioFilter`, ({ hasFeatureFlagEnabled, paddingClass }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ searchPageVerticalNav: hasFeatureFlagEnabled,
+ },
+ },
});
});
+
+ it(`has ${paddingClass} class`, () => {
+ expect(findRadioFilter().classes(paddingClass)).toBe(true);
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js
index 4f217709297..7e564bfa005 100644
--- a/spec/frontend/search/sidebar/components/filters_spec.js
+++ b/spec/frontend/search/sidebar/components/filters_spec.js
@@ -129,4 +129,44 @@ describe('GlobalSearchSidebarFilters', () => {
expect(actionSpies.resetQuery).toHaveBeenCalled();
});
});
+
+ describe.each`
+ scope | showFilter
+ ${'issues'} | ${true}
+ ${'merge_requests'} | ${false}
+ ${'projects'} | ${false}
+ ${'milestones'} | ${false}
+ ${'users'} | ${false}
+ ${'notes'} | ${false}
+ ${'wiki_blobs'} | ${false}
+ ${'blobs'} | ${false}
+ `(`ConfidentialityFilter`, ({ scope, showFilter }) => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { scope } });
+ });
+
+ it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
+ expect(findConfidentialityFilter().exists()).toBe(showFilter);
+ });
+ });
+
+ describe.each`
+ scope | showFilter
+ ${'issues'} | ${true}
+ ${'merge_requests'} | ${true}
+ ${'projects'} | ${false}
+ ${'milestones'} | ${false}
+ ${'users'} | ${false}
+ ${'notes'} | ${false}
+ ${'wiki_blobs'} | ${false}
+ ${'blobs'} | ${false}
+ `(`StatusFilter`, ({ scope, showFilter }) => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { scope } });
+ });
+
+ it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
+ expect(findStatusFilter().exists()).toBe(showFilter);
+ });
+ });
});
diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
index 6262a52e01a..23c158239dc 100644
--- a/spec/frontend/search/sidebar/components/scope_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
@@ -1,4 +1,4 @@
-import { GlNav, GlNavItem } from '@gitlab/ui';
+import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -37,6 +37,7 @@ describe('ScopeNavigation', () => {
const findGlNav = () => wrapper.findComponent(GlNav);
const findGlNavItems = () => wrapper.findAllComponents(GlNavItem);
const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active'));
+ const findGlNavItemActiveLabel = () => findGlNavItemActive().at(0).findAll('span').at(0).text();
const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1);
describe('scope navigation', () => {
@@ -56,7 +57,7 @@ describe('ScopeNavigation', () => {
expect(findGlNavItems()).toHaveLength(9);
});
- it('nav items have proper links', () => {
+ it('has all proper links', () => {
const linkAtPosition = 3;
const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
@@ -64,17 +65,47 @@ describe('ScopeNavigation', () => {
});
});
- describe('scope navigation sets proper state', () => {
+ describe('scope navigation sets proper state with url scope set', () => {
beforeEach(() => {
createComponent();
});
- it('sets proper class to active item', () => {
+ it('has correct active item', () => {
expect(findGlNavItemActive()).toHaveLength(1);
+ expect(findGlNavItemActiveLabel()).toBe('Issues');
});
- it('active item', () => {
+ it('has correct active item count', () => {
expect(findGlNavItemActiveCount().text()).toBe('2.4K');
});
+
+ it('does not have plus sign after count text', () => {
+ expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(false);
+ });
+
+ it('has count is highlighted correctly', () => {
+ expect(findGlNavItemActiveCount().classes('gl-text-gray-900')).toBe(true);
+ });
+ });
+
+ describe('scope navigation sets proper state with NO url scope set', () => {
+ beforeEach(() => {
+ createComponent({
+ urlQuery: {},
+ });
+ });
+
+ it('has correct active item', () => {
+ expect(findGlNavItems().at(0).attributes('active')).toBe('true');
+ expect(findGlNavItemActiveLabel()).toBe('Projects');
+ });
+
+ it('has correct active item count', () => {
+ expect(findGlNavItemActiveCount().text()).toBe('10K');
+ });
+
+ it('has correct active item count and over limit sign', () => {
+ expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index f3152c014b6..2ed199469e6 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -1,39 +1,16 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { MOCK_QUERY } from 'jest/search/mock_data';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
-Vue.use(Vuex);
-
describe('StatusFilter', () => {
let wrapper;
- const actionSpies = {
- applyQuery: jest.fn(),
- resetQuery: jest.fn(),
- };
-
- const createComponent = (initialState) => {
- const store = new Vuex.Store({
- state: {
- query: MOCK_QUERY,
- ...initialState,
- },
- actions: actionSpies,
- });
-
+ const createComponent = (initProps) => {
wrapper = shallowMount(StatusFilter, {
- store,
+ ...initProps,
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
describe('template', () => {
@@ -41,24 +18,28 @@ describe('StatusFilter', () => {
createComponent();
});
- describe.each`
- scope | showFilter
- ${'issues'} | ${true}
- ${'merge_requests'} | ${true}
- ${'projects'} | ${false}
- ${'milestones'} | ${false}
- ${'users'} | ${false}
- ${'notes'} | ${false}
- ${'wiki_blobs'} | ${false}
- ${'blobs'} | ${false}
- `(`dropdown`, ({ scope, showFilter }) => {
- beforeEach(() => {
- createComponent({ query: { scope } });
- });
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+ });
- it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
- expect(findRadioFilter().exists()).toBe(showFilter);
+ describe.each`
+ hasFeatureFlagEnabled | paddingClass
+ ${true} | ${'gl-px-5'}
+ ${false} | ${'gl-px-0'}
+ `(`RadioFilter`, ({ hasFeatureFlagEnabled, paddingClass }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ searchPageVerticalNav: hasFeatureFlagEnabled,
+ },
+ },
});
});
+
+ it(`has ${paddingClass} class`, () => {
+ expect(findRadioFilter().classes(paddingClass)).toBe(true);
+ });
});
});
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index c7fd7084101..3975887cfff 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -1,4 +1,4 @@
-import { GlSearchBoxByClick } from '@gitlab/ui';
+import { GlSearchBoxByClick, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -6,6 +6,8 @@ import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
+import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
+import { SYNTAX_OPTIONS_DOCUMENT } from '~/search/topbar/constants';
Vue.use(Vuex);
@@ -18,7 +20,7 @@ describe('GlobalSearchTopbar', () => {
preloadStoredFrequentItems: jest.fn(),
};
- const createComponent = (initialState) => {
+ const createComponent = (initialState, props, stubs) => {
const store = new Vuex.Store({
state: {
query: MOCK_QUERY,
@@ -29,6 +31,8 @@ describe('GlobalSearchTopbar', () => {
wrapper = shallowMount(GlobalSearchTopbar, {
store,
+ propsData: props,
+ stubs,
});
};
@@ -39,6 +43,8 @@ describe('GlobalSearchTopbar', () => {
const findGlSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
const findGroupFilter = () => wrapper.findComponent(GroupFilter);
const findProjectFilter = () => wrapper.findComponent(ProjectFilter);
+ const findSyntaxOptionButton = () => wrapper.findComponent(GlButton);
+ const findSyntaxOptionDrawer = () => wrapper.findComponent(MarkdownDrawer);
describe('template', () => {
beforeEach(() => {
@@ -71,6 +77,72 @@ describe('GlobalSearchTopbar', () => {
expect(findProjectFilter().exists()).toBe(showFilters);
});
});
+
+ describe('syntax option feature', () => {
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent(
+ { query: { repository_ref: '' } },
+ { elasticsearchEnabled: true, defaultBranchName: '' },
+ );
+ });
+
+ it('renders button correctly', () => {
+ expect(findSyntaxOptionButton().exists()).toBe(true);
+ });
+
+ it('renders drawer correctly', () => {
+ expect(findSyntaxOptionDrawer().exists()).toBe(true);
+ expect(findSyntaxOptionDrawer().attributes('documentpath')).toBe(SYNTAX_OPTIONS_DOCUMENT);
+ });
+
+ it('dispatched correct click action', () => {
+ const draweToggleSpy = jest.fn();
+ wrapper.vm.$refs.markdownDrawer.toggleDrawer = draweToggleSpy;
+
+ findSyntaxOptionButton().vm.$emit('click');
+ expect(draweToggleSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe.each`
+ query | propsData | hasSyntaxOptions
+ ${null} | ${{ elasticsearchEnabled: false, defaultBranchName: '' }} | ${false}
+ ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: false, defaultBranchName: '' }} | ${false}
+ ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: false, defaultBranchName: 'master' }} | ${false}
+ ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: '' }} | ${false}
+ ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: 'master' }} | ${true}
+ ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: '' }} | ${true}
+ ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: 'master' }} | ${true}
+ `(
+ 'renders the syntax option based on component state',
+ ({ query, propsData, hasSyntaxOptions }) => {
+ beforeEach(() => {
+ createComponent(query, { ...propsData });
+ });
+
+ it(`does${
+ hasSyntaxOptions ? '' : ' not'
+ } have syntax option button when repository_ref: '${
+ query?.query?.repository_ref
+ }', elasticsearchEnabled: ${propsData.elasticsearchEnabled}, defaultBranchName: '${
+ propsData.defaultBranchName
+ }'`, () => {
+ expect(findSyntaxOptionButton().exists()).toBe(hasSyntaxOptions);
+ });
+
+ it(`does${
+ hasSyntaxOptions ? '' : ' not'
+ } have syntax option drawer when repository_ref: '${
+ query?.query?.repository_ref
+ }', elasticsearchEnabled: ${propsData.elasticsearchEnabled}, defaultBranchName: '${
+ propsData.defaultBranchName
+ }'`, () => {
+ expect(findSyntaxOptionDrawer().exists()).toBe(hasSyntaxOptions);
+ });
+ },
+ );
+ });
});
describe('actions', () => {
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 21e63533c66..65c9d2f5f01 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import * as actions from '~/self_monitor/store/actions';
import * as types from '~/self_monitor/store/mutation_types';
import createState from '~/self_monitor/store/state';
@@ -44,7 +44,7 @@ describe('self-monitor actions', () => {
beforeEach(() => {
state.createProjectEndpoint = '/create';
state.createProjectStatusEndpoint = '/create_status';
- mock.onPost(state.createProjectEndpoint).reply(statusCodes.ACCEPTED, {
+ mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '123',
});
mock.onGet(state.createProjectStatusEndpoint).reply(statusCodes.OK, {
@@ -151,7 +151,7 @@ describe('self-monitor actions', () => {
beforeEach(() => {
state.deleteProjectEndpoint = '/delete';
state.deleteProjectStatusEndpoint = '/delete-status';
- mock.onDelete(state.deleteProjectEndpoint).reply(statusCodes.ACCEPTED, {
+ mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '456',
});
mock.onGet(state.deleteProjectStatusEndpoint).reply(statusCodes.OK, {
diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js
index d1f098112e8..2dd528a8a1c 100644
--- a/spec/frontend/sentry/index_spec.js
+++ b/spec/frontend/sentry/index_spec.js
@@ -1,17 +1,20 @@
import index from '~/sentry/index';
+
+import LegacySentryConfig from '~/sentry/legacy_sentry_config';
import SentryConfig from '~/sentry/sentry_config';
-describe('SentryConfig options', () => {
+describe('Sentry init', () => {
+ let originalGon;
+
const dsn = 'https://123@sentry.gitlab.test/123';
- const currentUserId = 'currentUserId';
- const gitlabUrl = 'gitlabUrl';
const environment = 'test';
+ const currentUserId = '1';
+ const gitlabUrl = 'gitlabUrl';
const revision = 'revision';
const featureCategory = 'my_feature_category';
- let indexReturnValue;
-
beforeEach(() => {
+ originalGon = window.gon;
window.gon = {
sentry_dsn: dsn,
sentry_environment: environment,
@@ -21,28 +24,41 @@ describe('SentryConfig options', () => {
feature_category: featureCategory,
};
- process.env.HEAD_COMMIT_SHA = revision;
-
+ jest.spyOn(LegacySentryConfig, 'init').mockImplementation();
jest.spyOn(SentryConfig, 'init').mockImplementation();
+ });
- indexReturnValue = index();
+ afterEach(() => {
+ window.gon = originalGon;
});
- it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => {
- expect(SentryConfig.init).toHaveBeenCalledWith({
- dsn,
- currentUserId,
- whitelistUrls: [gitlabUrl, 'webpack-internal://'],
- environment,
- release: revision,
- tags: {
- revision,
- feature_category: featureCategory,
- },
- });
+ it('exports new version of Sentry in the global object', () => {
+ // eslint-disable-next-line no-underscore-dangle
+ expect(window._Sentry.SDK_VERSION).not.toMatch(/^5\./);
});
- it('should return SentryConfig', () => {
- expect(indexReturnValue).toBe(SentryConfig);
+ describe('when called', () => {
+ beforeEach(() => {
+ index();
+ });
+
+ it('configures sentry', () => {
+ expect(SentryConfig.init).toHaveBeenCalledTimes(1);
+ expect(SentryConfig.init).toHaveBeenCalledWith({
+ dsn,
+ currentUserId,
+ allowUrls: [gitlabUrl, 'webpack-internal://'],
+ environment,
+ release: revision,
+ tags: {
+ revision,
+ feature_category: featureCategory,
+ },
+ });
+ });
+
+ it('does not configure legacy sentry', () => {
+ expect(LegacySentryConfig.init).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/sentry/legacy_index_spec.js b/spec/frontend/sentry/legacy_index_spec.js
new file mode 100644
index 00000000000..5c336f8392e
--- /dev/null
+++ b/spec/frontend/sentry/legacy_index_spec.js
@@ -0,0 +1,64 @@
+import index from '~/sentry/legacy_index';
+
+import LegacySentryConfig from '~/sentry/legacy_sentry_config';
+import SentryConfig from '~/sentry/sentry_config';
+
+describe('Sentry init', () => {
+ let originalGon;
+
+ const dsn = 'https://123@sentry.gitlab.test/123';
+ const environment = 'test';
+ const currentUserId = '1';
+ const gitlabUrl = 'gitlabUrl';
+ const revision = 'revision';
+ const featureCategory = 'my_feature_category';
+
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = {
+ sentry_dsn: dsn,
+ sentry_environment: environment,
+ current_user_id: currentUserId,
+ gitlab_url: gitlabUrl,
+ revision,
+ feature_category: featureCategory,
+ };
+
+ jest.spyOn(LegacySentryConfig, 'init').mockImplementation();
+ jest.spyOn(SentryConfig, 'init').mockImplementation();
+ });
+
+ afterEach(() => {
+ window.gon = originalGon;
+ });
+
+ it('exports legacy version of Sentry in the global object', () => {
+ // eslint-disable-next-line no-underscore-dangle
+ expect(window._Sentry.SDK_VERSION).toMatch(/^5\./);
+ });
+
+ describe('when called', () => {
+ beforeEach(() => {
+ index();
+ });
+
+ it('configures legacy sentry', () => {
+ expect(LegacySentryConfig.init).toHaveBeenCalledTimes(1);
+ expect(LegacySentryConfig.init).toHaveBeenCalledWith({
+ dsn,
+ currentUserId,
+ whitelistUrls: [gitlabUrl, 'webpack-internal://'],
+ environment,
+ release: revision,
+ tags: {
+ revision,
+ feature_category: featureCategory,
+ },
+ });
+ });
+
+ it('does not configure new sentry', () => {
+ expect(SentryConfig.init).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/sentry/legacy_sentry_config_spec.js b/spec/frontend/sentry/legacy_sentry_config_spec.js
new file mode 100644
index 00000000000..fe90cb49074
--- /dev/null
+++ b/spec/frontend/sentry/legacy_sentry_config_spec.js
@@ -0,0 +1,215 @@
+import * as Sentry5 from 'sentrybrowser5';
+import LegacySentryConfig from '~/sentry/legacy_sentry_config';
+
+describe('LegacySentryConfig', () => {
+ describe('IGNORE_ERRORS', () => {
+ it('should be an array of strings', () => {
+ const areStrings = LegacySentryConfig.IGNORE_ERRORS.every(
+ (error) => typeof error === 'string',
+ );
+
+ expect(areStrings).toBe(true);
+ });
+ });
+
+ describe('BLACKLIST_URLS', () => {
+ it('should be an array of regexps', () => {
+ const areRegExps = LegacySentryConfig.BLACKLIST_URLS.every((url) => url instanceof RegExp);
+
+ expect(areRegExps).toBe(true);
+ });
+ });
+
+ describe('SAMPLE_RATE', () => {
+ it('should be a finite number', () => {
+ expect(typeof LegacySentryConfig.SAMPLE_RATE).toEqual('number');
+ });
+ });
+
+ describe('init', () => {
+ const options = {
+ currentUserId: 1,
+ };
+
+ beforeEach(() => {
+ jest.spyOn(LegacySentryConfig, 'configure');
+ jest.spyOn(LegacySentryConfig, 'bindSentryErrors');
+ jest.spyOn(LegacySentryConfig, 'setUser');
+
+ LegacySentryConfig.init(options);
+ });
+
+ it('should set the options property', () => {
+ expect(LegacySentryConfig.options).toEqual(options);
+ });
+
+ it('should call the configure method', () => {
+ expect(LegacySentryConfig.configure).toHaveBeenCalled();
+ });
+
+ it('should call the error bindings method', () => {
+ expect(LegacySentryConfig.bindSentryErrors).toHaveBeenCalled();
+ });
+
+ it('should call setUser', () => {
+ expect(LegacySentryConfig.setUser).toHaveBeenCalled();
+ });
+
+ it('should not call setUser if there is no current user ID', () => {
+ LegacySentryConfig.setUser.mockClear();
+ options.currentUserId = undefined;
+
+ LegacySentryConfig.init(options);
+
+ expect(LegacySentryConfig.setUser).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('configure', () => {
+ const sentryConfig = {};
+ const options = {
+ dsn: 'https://123@sentry.gitlab.test/123',
+ whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ environment: 'test',
+ release: 'revision',
+ tags: {
+ revision: 'revision',
+ feature_category: 'my_feature_category',
+ },
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Sentry5, 'init').mockImplementation();
+ jest.spyOn(Sentry5, 'setTags').mockImplementation();
+
+ sentryConfig.options = options;
+ sentryConfig.IGNORE_ERRORS = 'ignore_errors';
+ sentryConfig.BLACKLIST_URLS = 'blacklist_urls';
+
+ LegacySentryConfig.configure.call(sentryConfig);
+ });
+
+ it('should call Sentry5.init', () => {
+ expect(Sentry5.init).toHaveBeenCalledWith({
+ dsn: options.dsn,
+ release: options.release,
+ sampleRate: 0.95,
+ whitelistUrls: options.whitelistUrls,
+ environment: 'test',
+ ignoreErrors: sentryConfig.IGNORE_ERRORS,
+ blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ });
+ });
+
+ it('should call Sentry5.setTags', () => {
+ expect(Sentry5.setTags).toHaveBeenCalledWith(options.tags);
+ });
+
+ it('should set environment from options', () => {
+ sentryConfig.options.environment = 'development';
+
+ LegacySentryConfig.configure.call(sentryConfig);
+
+ expect(Sentry5.init).toHaveBeenCalledWith({
+ dsn: options.dsn,
+ release: options.release,
+ sampleRate: 0.95,
+ whitelistUrls: options.whitelistUrls,
+ environment: 'development',
+ ignoreErrors: sentryConfig.IGNORE_ERRORS,
+ blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ });
+ });
+ });
+
+ describe('setUser', () => {
+ let sentryConfig;
+
+ beforeEach(() => {
+ sentryConfig = { options: { currentUserId: 1 } };
+ jest.spyOn(Sentry5, 'setUser');
+
+ LegacySentryConfig.setUser.call(sentryConfig);
+ });
+
+ it('should call .setUser', () => {
+ expect(Sentry5.setUser).toHaveBeenCalledWith({
+ id: sentryConfig.options.currentUserId,
+ });
+ });
+ });
+
+ describe('handleSentryErrors', () => {
+ let event;
+ let req;
+ let config;
+ let err;
+
+ beforeEach(() => {
+ event = {};
+ req = { status: 'status', responseText: 'Unknown response text', statusText: 'statusText' };
+ config = { type: 'type', url: 'url', data: 'data' };
+ err = {};
+
+ jest.spyOn(Sentry5, 'captureMessage');
+
+ LegacySentryConfig.handleSentryErrors(event, req, config, err);
+ });
+
+ it('should call Sentry5.captureMessage', () => {
+ expect(Sentry5.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: err,
+ event,
+ },
+ });
+ });
+
+ describe('if no err is provided', () => {
+ beforeEach(() => {
+ LegacySentryConfig.handleSentryErrors(event, req, config);
+ });
+
+ it('should use req.statusText as the error value', () => {
+ expect(Sentry5.captureMessage).toHaveBeenCalledWith(req.statusText, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: req.statusText,
+ event,
+ },
+ });
+ });
+ });
+
+ describe('if no req.responseText is provided', () => {
+ beforeEach(() => {
+ req.responseText = undefined;
+
+ LegacySentryConfig.handleSentryErrors(event, req, config, err);
+ });
+
+ it('should use `Unknown response text` as the response', () => {
+ expect(Sentry5.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: 'Unknown response text',
+ error: err,
+ event,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sentry/sentry_browser_wrapper_spec.js b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
new file mode 100644
index 00000000000..f4d646bab78
--- /dev/null
+++ b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
@@ -0,0 +1,59 @@
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+
+const mockError = new Error('error!');
+const mockMsg = 'msg!';
+const mockFn = () => {};
+
+describe('SentryBrowserWrapper', () => {
+ afterEach(() => {
+ // eslint-disable-next-line no-underscore-dangle
+ delete window._Sentry;
+ });
+
+ describe('when _Sentry is not defined', () => {
+ it('methods fail silently', () => {
+ expect(() => {
+ Sentry.captureException(mockError);
+ Sentry.captureMessage(mockMsg);
+ Sentry.withScope(mockFn);
+ }).not.toThrow();
+ });
+ });
+
+ describe('when _Sentry is defined', () => {
+ let mockCaptureException;
+ let mockCaptureMessage;
+ let mockWithScope;
+
+ beforeEach(async () => {
+ mockCaptureException = jest.fn();
+ mockCaptureMessage = jest.fn();
+ mockWithScope = jest.fn();
+
+ // eslint-disable-next-line no-underscore-dangle
+ window._Sentry = {
+ captureException: mockCaptureException,
+ captureMessage: mockCaptureMessage,
+ withScope: mockWithScope,
+ };
+ });
+
+ it('captureException is called', () => {
+ Sentry.captureException(mockError);
+
+ expect(mockCaptureException).toHaveBeenCalledWith(mockError);
+ });
+
+ it('captureMessage is called', () => {
+ Sentry.captureMessage(mockMsg);
+
+ expect(mockCaptureMessage).toHaveBeenCalledWith(mockMsg);
+ });
+
+ it('withScope is called', () => {
+ Sentry.withScope(mockFn);
+
+ expect(mockWithScope).toHaveBeenCalledWith(mockFn);
+ });
+ });
+});
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
index 9f67b681b8d..44acbee9b38 100644
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -1,29 +1,9 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from 'sentrybrowser7';
+import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from '~/sentry/constants';
+
import SentryConfig from '~/sentry/sentry_config';
describe('SentryConfig', () => {
- describe('IGNORE_ERRORS', () => {
- it('should be an array of strings', () => {
- const areStrings = SentryConfig.IGNORE_ERRORS.every((error) => typeof error === 'string');
-
- expect(areStrings).toBe(true);
- });
- });
-
- describe('BLACKLIST_URLS', () => {
- it('should be an array of regexps', () => {
- const areRegExps = SentryConfig.BLACKLIST_URLS.every((url) => url instanceof RegExp);
-
- expect(areRegExps).toBe(true);
- });
- });
-
- describe('SAMPLE_RATE', () => {
- it('should be a finite number', () => {
- expect(typeof SentryConfig.SAMPLE_RATE).toEqual('number');
- });
- });
-
describe('init', () => {
const options = {
currentUserId: 1,
@@ -31,7 +11,6 @@ describe('SentryConfig', () => {
beforeEach(() => {
jest.spyOn(SentryConfig, 'configure');
- jest.spyOn(SentryConfig, 'bindSentryErrors');
jest.spyOn(SentryConfig, 'setUser');
SentryConfig.init(options);
@@ -45,19 +24,13 @@ describe('SentryConfig', () => {
expect(SentryConfig.configure).toHaveBeenCalled();
});
- it('should call the error bindings method', () => {
- expect(SentryConfig.bindSentryErrors).toHaveBeenCalled();
- });
-
it('should call setUser', () => {
expect(SentryConfig.setUser).toHaveBeenCalled();
});
it('should not call setUser if there is no current user ID', () => {
SentryConfig.setUser.mockClear();
- options.currentUserId = undefined;
-
- SentryConfig.init(options);
+ SentryConfig.init({ currentUserId: undefined });
expect(SentryConfig.setUser).not.toHaveBeenCalled();
});
@@ -67,7 +40,7 @@ describe('SentryConfig', () => {
const sentryConfig = {};
const options = {
dsn: 'https://123@sentry.gitlab.test/123',
- whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ allowUrls: ['//gitlabUrl', 'webpack-internal://'],
environment: 'test',
release: 'revision',
tags: {
@@ -81,8 +54,6 @@ describe('SentryConfig', () => {
jest.spyOn(Sentry, 'setTags').mockImplementation();
sentryConfig.options = options;
- sentryConfig.IGNORE_ERRORS = 'ignore_errors';
- sentryConfig.BLACKLIST_URLS = 'blacklist_urls';
SentryConfig.configure.call(sentryConfig);
});
@@ -91,11 +62,11 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: 0.95,
- whitelistUrls: options.whitelistUrls,
- environment: 'test',
- ignoreErrors: sentryConfig.IGNORE_ERRORS,
- blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ sampleRate: SAMPLE_RATE,
+ allowUrls: options.allowUrls,
+ environment: options.environment,
+ ignoreErrors: IGNORE_ERRORS,
+ denyUrls: DENY_URLS,
});
});
@@ -111,11 +82,11 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: 0.95,
- whitelistUrls: options.whitelistUrls,
+ sampleRate: SAMPLE_RATE,
+ allowUrls: options.allowUrls,
environment: 'development',
- ignoreErrors: sentryConfig.IGNORE_ERRORS,
- blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ ignoreErrors: IGNORE_ERRORS,
+ denyUrls: DENY_URLS,
});
});
});
@@ -136,78 +107,4 @@ describe('SentryConfig', () => {
});
});
});
-
- describe('handleSentryErrors', () => {
- let event;
- let req;
- let config;
- let err;
-
- beforeEach(() => {
- event = {};
- req = { status: 'status', responseText: 'Unknown response text', statusText: 'statusText' };
- config = { type: 'type', url: 'url', data: 'data' };
- err = {};
-
- jest.spyOn(Sentry, 'captureMessage');
-
- SentryConfig.handleSentryErrors(event, req, config, err);
- });
-
- it('should call Sentry.captureMessage', () => {
- expect(Sentry.captureMessage).toHaveBeenCalledWith(err, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: req.responseText,
- error: err,
- event,
- },
- });
- });
-
- describe('if no err is provided', () => {
- beforeEach(() => {
- SentryConfig.handleSentryErrors(event, req, config);
- });
-
- it('should use req.statusText as the error value', () => {
- expect(Sentry.captureMessage).toHaveBeenCalledWith(req.statusText, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: req.responseText,
- error: req.statusText,
- event,
- },
- });
- });
- });
-
- describe('if no req.responseText is provided', () => {
- beforeEach(() => {
- req.responseText = undefined;
-
- SentryConfig.handleSentryErrors(event, req, config, err);
- });
-
- it('should use `Unknown response text` as the response', () => {
- expect(Sentry.captureMessage).toHaveBeenCalledWith(err, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: 'Unknown response text',
- error: err,
- event,
- },
- });
- });
- });
- });
});
diff --git a/spec/frontend/sidebar/assignee_title_spec.js b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
index 14a6bdbf907..14a6bdbf907 100644
--- a/spec/frontend/sidebar/assignee_title_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
index ae8f07bf901..080171fb2ea 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
@@ -6,12 +6,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
+import getIssueAssigneesQuery from '~/sidebar/queries/get_issue_assignees.query.graphql';
import Mock, {
issuableQueryResponse,
subscriptionNullResponse,
subscriptionResponse,
-} from './mock_data';
+} from '../../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index 7cf7fd33022..6971ae2f9ed 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -5,7 +5,7 @@ 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';
+import UsersMock from '../../mock_data';
describe('Assignee component', () => {
const getDefaultProps = () => ({
diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
index 1161fefcc64..1161fefcc64 100644
--- a/spec/frontend/sidebar/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
index 2cb2425532b..58b174059fa 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
@@ -8,7 +8,7 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.v
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
+import Mock from '../../mock_data';
describe('sidebar assignees', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index cbb4c41dd14..3aca346ff5f 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -12,8 +12,8 @@ import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import getIssueAssigneesQuery from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
-import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
+import getIssueAssigneesQuery from '~/sidebar/queries/get_issue_assignees.query.graphql';
+import updateIssueAssigneesMutation from '~/sidebar/queries/update_issue_assignees.mutation.graphql';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data';
diff --git a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js b/spec/frontend/sidebar/components/copy/copy_email_to_clipboard_spec.js
index 69a8d645973..5b6db43a366 100644
--- a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
+++ b/spec/frontend/sidebar/components/copy/copy_email_to_clipboard_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import CopyEmailToClipboard from '~/sidebar/components/copy_email_to_clipboard.vue';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyEmailToClipboard from '~/sidebar/components/copy/copy_email_to_clipboard.vue';
+import CopyableField from '~/sidebar/components/copy/copyable_field.vue';
describe('CopyEmailToClipboard component', () => {
const mockIssueEmailAddress = 'sample+email@test.com';
diff --git a/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
index 3980033862e..7790d77bc65 100644
--- a/spec/frontend/vue_shared/components/sidebar/copyable_field_spec.js
+++ b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyableField from '~/sidebar/components/copy/copyable_field.vue';
describe('SidebarCopyableField', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
index 69e35cd1d05..c5161a748a9 100644
--- a/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
@@ -4,10 +4,10 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { IssuableType } from '~/issues/constants';
-import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
+import SidebarReferenceWidget from '~/sidebar/components/copy/sidebar_reference_widget.vue';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyableField from '~/sidebar/components/copy/copyable_field.vue';
import { issueReferenceResponse } from '../../mock_data';
describe('Sidebar Reference Widget', () => {
diff --git a/spec/frontend/sidebar/components/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
index 6d76fa1f9df..ca43c219d92 100644
--- a/spec/frontend/sidebar/components/crm_contacts_spec.js
+++ b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
@@ -5,13 +5,13 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import CrmContacts from '~/sidebar/components/crm_contacts/crm_contacts.vue';
-import getIssueCrmContactsQuery from '~/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql';
-import issueCrmContactsSubscription from '~/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql';
+import getIssueCrmContactsQuery from '~/sidebar/queries/get_issue_crm_contacts.query.graphql';
+import issueCrmContactsSubscription from '~/sidebar/queries/issue_crm_contacts.subscription.graphql';
import {
getIssueCrmContactsQueryResponse,
issueCrmContactsUpdateResponse,
issueCrmContactsUpdateNullResponse,
-} from './mock_data';
+} from '../mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
index 83764cb6739..1a78ce4ddee 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
@@ -3,11 +3,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EscalationStatus from '~/sidebar/components/incidents/escalation_status.vue';
-import {
- STATUS_LABELS,
- STATUS_TRIGGERED,
- STATUS_ACKNOWLEDGED,
-} from '~/sidebar/components/incidents/constants';
+import { STATUS_LABELS, STATUS_TRIGGERED, STATUS_ACKNOWLEDGED } from '~/sidebar/constants';
describe('EscalationStatus', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js b/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js
index edd65db0325..d9e7f29c10e 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_utils_spec.js
@@ -1,5 +1,5 @@
-import { STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants';
-import { getStatusLabel } from '~/sidebar/components/incidents/utils';
+import { STATUS_ACKNOWLEDGED } from '~/sidebar/constants';
+import { getStatusLabel } from '~/sidebar/utils';
describe('EscalationUtils', () => {
describe('getStatusLabel', () => {
diff --git a/spec/frontend/sidebar/components/incidents/mock_data.js b/spec/frontend/sidebar/components/incidents/mock_data.js
index bbb6c61b162..2a5b7798110 100644
--- a/spec/frontend/sidebar/components/incidents/mock_data.js
+++ b/spec/frontend/sidebar/components/incidents/mock_data.js
@@ -1,4 +1,4 @@
-import { STATUS_TRIGGERED, STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants';
+import { STATUS_TRIGGERED, STATUS_ACKNOWLEDGED } from '~/sidebar/constants';
export const fetchData = {
workspace: {
diff --git a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
index 88a4913a27f..2dded61c073 100644
--- a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
@@ -1,5 +1,4 @@
-import { createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import {
fetchData,
@@ -12,26 +11,28 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SidebarEscalationStatus from '~/sidebar/components/incidents/sidebar_escalation_status.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { escalationStatusQuery, escalationStatusMutation } from '~/sidebar/constants';
+import {
+ escalationStatusQuery,
+ escalationStatusMutation,
+ STATUS_ACKNOWLEDGED,
+} from '~/sidebar/constants';
import waitForPromises from 'helpers/wait_for_promises';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
-import { STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants';
import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
jest.mock('~/lib/logger');
jest.mock('~/flash');
-const localVue = createLocalVue();
+Vue.use(VueApollo);
describe('SidebarEscalationStatus', () => {
let wrapper;
+ let mockApollo;
const queryResolverMock = jest.fn();
const mutationResolverMock = jest.fn();
function createMockApolloProvider({ hasFetchError = false, hasMutationError = false } = {}) {
- localVue.use(VueApollo);
-
queryResolverMock.mockResolvedValue({ data: hasFetchError ? fetchError : fetchData });
mutationResolverMock.mockResolvedValue({
data: hasMutationError ? mutationError : mutationData,
@@ -45,15 +46,7 @@ describe('SidebarEscalationStatus', () => {
return createMockApollo(requestHandlers);
}
- function createComponent({ mockApollo } = {}) {
- let config;
-
- if (mockApollo) {
- config = { apolloProvider: mockApollo };
- } else {
- config = { mocks: { $apollo: { queries: { status: { loading: false } } } } };
- }
-
+ function createComponent(apolloProvider) {
wrapper = mountExtended(SidebarEscalationStatus, {
propsData: {
iid: '1',
@@ -66,13 +59,15 @@ describe('SidebarEscalationStatus', () => {
directives: {
GlTooltip: createMockDirective(),
},
- localVue,
- ...config,
+ apolloProvider,
});
+
+ // wait for apollo requests
+ return waitForPromises();
}
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ mockApollo = createMockApolloProvider();
});
const findSidebarComponent = () => wrapper.findComponent(SidebarEditableItem);
@@ -80,36 +75,32 @@ describe('SidebarEscalationStatus', () => {
const findEditButton = () => wrapper.findByTestId('edit-button');
const findIcon = () => wrapper.findByTestId('status-icon');
- const clickEditButton = async () => {
+ const clickEditButton = () => {
findEditButton().vm.$emit('click');
- await nextTick();
+ return nextTick();
};
- const selectAcknowledgedStatus = async () => {
+ const selectAcknowledgedStatus = () => {
findStatusComponent().vm.$emit('input', STATUS_ACKNOWLEDGED);
// wait for apollo requests
- await waitForPromises();
+ return waitForPromises();
};
describe('sidebar', () => {
- it('renders the sidebar component', () => {
- createComponent();
+ it('renders the sidebar component', async () => {
+ await createComponent(mockApollo);
expect(findSidebarComponent().exists()).toBe(true);
});
describe('status icon', () => {
- it('is visible', () => {
- createComponent();
+ it('is visible', async () => {
+ await createComponent(mockApollo);
expect(findIcon().exists()).toBe(true);
expect(findIcon().isVisible()).toBe(true);
});
it('has correct tooltip', async () => {
- const mockApollo = createMockApolloProvider();
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ await createComponent(mockApollo);
const tooltip = getBinding(findIcon().element, 'gl-tooltip');
@@ -120,11 +111,7 @@ describe('SidebarEscalationStatus', () => {
describe('status dropdown', () => {
beforeEach(async () => {
- const mockApollo = createMockApolloProvider();
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ await createComponent(mockApollo);
});
it('is closed by default', () => {
@@ -148,11 +135,7 @@ describe('SidebarEscalationStatus', () => {
describe('update Status event', () => {
beforeEach(async () => {
- const mockApollo = createMockApolloProvider();
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ await createComponent(mockApollo);
await clickEditButton();
await selectAcknowledgedStatus();
@@ -184,22 +167,16 @@ describe('SidebarEscalationStatus', () => {
describe('mutation errors', () => {
it('should error upon fetch', async () => {
- const mockApollo = createMockApolloProvider({ hasFetchError: true });
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ mockApollo = createMockApolloProvider({ hasFetchError: true });
+ await createComponent(mockApollo);
expect(createAlert).toHaveBeenCalled();
expect(logError).toHaveBeenCalled();
});
it('should error upon mutation', async () => {
- const mockApollo = createMockApolloProvider({ hasMutationError: true });
- createComponent({ mockApollo });
-
- // wait for apollo requests
- await waitForPromises();
+ mockApollo = createMockApolloProvider({ hasMutationError: true });
+ await createComponent(mockApollo);
await clickEditButton();
await selectAcknowledgedStatus();
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
index c0e5408e1bd..4f2a89e20db 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
+import DropdownButton from '~/sidebar/components/labels/labels_select_vue/dropdown_button.vue';
-import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
index 799e2c1d08e..59e95edfa20 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue';
-import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig, mockSuggestedColors } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
index cc9b9f393ce..865dc8fe8fb 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -9,13 +9,13 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue';
+import LabelItem from '~/sidebar/components/labels/labels_select_vue/label_item.vue';
-import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
-import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
-import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
-import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
+import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
+import * as getters from '~/sidebar/components/labels/labels_select_vue/store/getters';
+import mutations from '~/sidebar/components/labels/labels_select_vue/store/mutations';
+import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
import { mockConfig, mockLabels, mockRegularLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
index 9781d9c4de0..e9ffda7c251 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
@@ -2,9 +2,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
+import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue';
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
index 54804f85f81..6c3fda421ff 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
+import DropdownTitle from '~/sidebar/components/labels/labels_select_vue/dropdown_title.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
index c6400320dea..56f25a1c6a4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
+import DropdownValueCollapsedComponent from '~/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue';
import { mockCollapsedLabels as mockLabels, mockRegularLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
index f3c4839002b..a1ccc9d2ab1 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
@@ -3,9 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_vue/dropdown_value.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig, mockLabels, mockRegularLabel, mockScopedLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
index bb0f1777de6..e14c0e308ce 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
@@ -1,7 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
+import LabelItem from '~/sidebar/components/labels/labels_select_vue/label_item.vue';
import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
index 30c1a4b7d2f..a3b10c18374 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
@@ -3,15 +3,15 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
-import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
-import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
+import DropdownButton from '~/sidebar/components/labels/labels_select_vue/dropdown_button.vue';
+import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue';
+import DropdownTitle from '~/sidebar/components/labels/labels_select_vue/dropdown_title.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_vue/dropdown_value.vue';
+import DropdownValueCollapsed from '~/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue';
+import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
+
+import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_vue/mock_data.js
index 884bc4684ba..884bc4684ba 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/mock_data.js
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
index edd044bd754..0e0024aa6c2 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
@@ -3,9 +3,9 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
-import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
-import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
+import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
+import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types';
+import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/getters_spec.js
index 6ad46dbe898..e32256831a3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/getters_spec.js
@@ -1,4 +1,4 @@
-import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
+import * as getters from '~/sidebar/components/labels/labels_select_vue/store/getters';
describe('LabelsSelect Getters', () => {
describe('dropdownButtonText', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/mutations_spec.js
index 2b2508b5e11..cee5d2e77d1 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/mutations_spec.js
@@ -1,6 +1,6 @@
import { cloneDeep } from 'lodash';
-import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
-import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
+import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types';
+import mutations from '~/sidebar/components/labels/labels_select_vue/store/mutations';
describe('LabelsSelect Mutations', () => {
describe(`${types.SET_INITIAL_STATE}`, () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
index 237f174e048..79b164b0ea7 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -6,8 +6,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { workspaceLabelsQueries } from '~/sidebar/constants';
-import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
-import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import createLabelMutation from '~/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
import {
mockRegularLabel,
mockSuggestedColors,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
index 5d8ad5ddee5..913badccbe4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -11,10 +11,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
-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 { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
index 00da9b74957..9bbb1413ee9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
@@ -1,10 +1,10 @@
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';
-import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
-import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
import { mockLabels } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
index 0508a059195..9a6e0ca3ccd 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
describe('DropdownFooter', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
index c4faef8ccdd..d9001dface4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
@@ -1,7 +1,7 @@
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
describe('DropdownHeader', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
index 0c4f4b7d504..585048983c9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
@@ -1,7 +1,7 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
import { mockRegularLabel, mockScopedLabel } from './mock_data';
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
new file mode 100644
index 00000000000..4fa65c752f9
--- /dev/null
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
@@ -0,0 +1,77 @@
+import { GlLabel } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EmbeddedLabelsList from '~/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue';
+import { mockRegularLabel, mockScopedLabel } from './mock_data';
+
+describe('EmbeddedLabelsList', () => {
+ let wrapper;
+
+ const findAllLabels = () => wrapper.findAllComponents(GlLabel);
+ const findLabelByTitle = (title) =>
+ findAllLabels()
+ .filter((label) => label.props('title') === title)
+ .at(0);
+ const findRegularLabel = () => findLabelByTitle(mockRegularLabel.title);
+ const findScopedLabel = () => findLabelByTitle(mockScopedLabel.title);
+
+ const createComponent = (props = {}, slots = {}) => {
+ wrapper = shallowMountExtended(EmbeddedLabelsList, {
+ slots,
+ propsData: {
+ selectedLabels: [mockRegularLabel, mockScopedLabel],
+ allowLabelRemove: true,
+ labelsFilterBasePath: '/gitlab-org/my-project/issues',
+ labelsFilterParam: 'label_name',
+ ...props,
+ },
+ provide: {
+ allowScopedLabels: true,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when there are no labels', () => {
+ beforeEach(() => {
+ createComponent({
+ selectedLabels: [],
+ });
+ });
+
+ it('does not render any labels', () => {
+ expect(findAllLabels()).toHaveLength(0);
+ });
+ });
+
+ describe('when there are labels', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a list of two labels', () => {
+ expect(findAllLabels()).toHaveLength(2);
+ });
+
+ it('passes correct props to the regular label', () => {
+ expect(findRegularLabel().props('target')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
+ );
+ expect(findRegularLabel().props('scoped')).toBe(false);
+ });
+
+ it('passes correct props to the scoped label', () => {
+ expect(findScopedLabel().props('target')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar',
+ );
+ expect(findScopedLabel().props('scoped')).toBe(true);
+ });
+
+ it('emits `onLabelRemove` event with the correct ID', () => {
+ findRegularLabel().vm.$emit('close');
+ expect(wrapper.emitted('onLabelRemove')).toStrictEqual([[mockRegularLabel.id]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
index 6e8841411a2..74188a77994 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
index 74ddd07d041..2995c268966 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
@@ -6,19 +6,22 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
-import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
+import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
+import EmbeddedLabelsList from '~/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue';
+import issueLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
-import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
-import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
+import updateEpicLabelsMutation from '~/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import {
mockConfig,
issuableLabelsQueryResponse,
updateLabelsMutationResponse,
issuableLabelsSubscriptionResponse,
+ mockLabels,
+ mockRegularLabel,
} from './mock_data';
jest.mock('~/flash');
@@ -42,6 +45,7 @@ describe('LabelsSelectRoot', () => {
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findDropdownValue = () => wrapper.findComponent(DropdownValue);
const findDropdownContents = () => wrapper.findComponent(DropdownContents);
+ const findEmbeddedLabelsList = () => wrapper.findComponent(EmbeddedLabelsList);
const createComponent = ({
config = mockConfig,
@@ -151,6 +155,52 @@ describe('LabelsSelectRoot', () => {
});
});
+ describe('if dropdown variant is `embedded`', () => {
+ it('shows the embedded labels list', () => {
+ createComponent({
+ config: { ...mockConfig, iid: '', variant: 'embedded', showEmbeddedLabelsList: true },
+ });
+
+ expect(findEmbeddedLabelsList().props()).toMatchObject({
+ disabled: false,
+ selectedLabels: [],
+ allowLabelRemove: false,
+ labelsFilterBasePath: mockConfig.labelsFilterBasePath,
+ labelsFilterParam: mockConfig.labelsFilterParam,
+ });
+ });
+
+ it('passes the selected labels if provided', () => {
+ createComponent({
+ config: {
+ ...mockConfig,
+ iid: '',
+ variant: 'embedded',
+ showEmbeddedLabelsList: true,
+ selectedLabels: mockLabels,
+ },
+ });
+
+ expect(findEmbeddedLabelsList().props('selectedLabels')).toStrictEqual(mockLabels);
+ expect(findDropdownContents().props('selectedLabels')).toStrictEqual(mockLabels);
+ });
+
+ it('emits the `onLabelRemove` when the embedded list triggers a removal', () => {
+ createComponent({
+ config: {
+ ...mockConfig,
+ iid: '',
+ variant: 'embedded',
+ showEmbeddedLabelsList: true,
+ selectedLabels: [mockRegularLabel],
+ },
+ });
+
+ findEmbeddedLabelsList().vm.$emit('onLabelRemove', [mockRegularLabel.id]);
+ expect(wrapper.emitted('onLabelRemove')).toStrictEqual([[[mockRegularLabel.id]]]);
+ });
+ });
+
it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
createComponent({ config: { ...mockConfig, iid: undefined } });
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
index 48530a0261f..48530a0261f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
diff --git a/spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
index 18d4df297df..18d4df297df 100644
--- a/spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap
+++ b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
diff --git a/spec/frontend/sidebar/lock/constants.js b/spec/frontend/sidebar/components/lock/constants.js
index b9f08e9286d..b9f08e9286d 100644
--- a/spec/frontend/sidebar/lock/constants.js
+++ b/spec/frontend/sidebar/components/lock/constants.js
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
index 2abb0c24d7d..2abb0c24d7d 100644
--- a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
diff --git a/spec/frontend/sidebar/lock/edit_form_spec.js b/spec/frontend/sidebar/components/lock/edit_form_spec.js
index 4ae9025ee39..4ae9025ee39 100644
--- a/spec/frontend/sidebar/lock/edit_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_spec.js
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index 8f825847cfc..8f825847cfc 100644
--- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
index d531147c0e6..72279f44e80 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
@@ -12,7 +12,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import axios from '~/lib/utils/axios_utils';
-import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
const mockProjects = [
{
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
index c432d722637..999340da27c 100644
--- a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
@@ -6,12 +6,12 @@ import { GlAlert } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
-import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
-import MoveIssuesButton from '~/issuable/bulk_update_sidebar/components/move_issues_button.vue';
+import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
import issuableEventHub from '~/issues/list/eventhub';
-import moveIssueMutation from '~/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql';
+import MoveIssuesButton from '~/sidebar/components/move/move_issues_button.vue';
+import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import { getIssuesCountsQueryResponse, getIssuesQueryResponse } from 'jest/issues/list/mock_data';
@@ -389,7 +389,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when only tasks are selected', async () => {
@@ -399,7 +399,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when only test cases are selected', async () => {
@@ -409,7 +409,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
@@ -419,7 +419,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('does not create flashes or logs errors when issues are moved without errors', async () => {
@@ -432,7 +432,7 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
});
it('creates a flash and logs errors when a mutation returns errors', async () => {
@@ -456,8 +456,8 @@ describe('MoveIssuesButton', () => {
);
// Only one flash is created even if multiple errors are reported
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while moving the issues.',
});
});
@@ -469,8 +469,8 @@ describe('MoveIssuesButton', () => {
await waitForPromises();
expect(logError).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while moving the issues.',
});
});
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js
index f7a626a189c..f7a626a189c 100644
--- a/spec/frontend/sidebar/participants_spec.js
+++ b/spec/frontend/sidebar/components/participants/participants_spec.js
diff --git a/spec/frontend/sidebar/reviewer_title_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
index 68ecd62e4c6..68ecd62e4c6 100644
--- a/spec/frontend/sidebar/reviewer_title_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
index 229f7ffbe04..229f7ffbe04 100644
--- a/spec/frontend/sidebar/reviewers_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
new file mode 100644
index 00000000000..57ae146a27a
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import axios from 'axios';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from '../../mock_data';
+
+Vue.use(VueApollo);
+
+describe('sidebar reviewers', () => {
+ const apolloMock = createMockApollo();
+ let wrapper;
+ let mediator;
+ let axiosMock;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(SidebarReviewers, {
+ apolloProvider: apolloMock,
+ propsData: {
+ issuableIid: '1',
+ issuableId: 1,
+ mediator,
+ field: '',
+ projectPath: 'projectPath',
+ changing: false,
+ ...props,
+ },
+ // Attaching to document is required because this component emits something from the parent element :/
+ attachTo: document.body,
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ mediator = new SidebarMediator(Mock.mediator);
+
+ jest.spyOn(mediator, 'saveReviewers');
+ jest.spyOn(mediator, 'addSelfReview');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ axiosMock.restore();
+ });
+
+ it('calls the mediator when it saves the reviewers', () => {
+ createComponent();
+
+ expect(mediator.saveReviewers).not.toHaveBeenCalled();
+
+ wrapper.vm.saveReviewers();
+
+ expect(mediator.saveReviewers).toHaveBeenCalled();
+ });
+
+ it('calls the mediator when "reviewBySelf" method is called', () => {
+ createComponent();
+
+ expect(mediator.addSelfReview).not.toHaveBeenCalled();
+ expect(mediator.store.reviewers.length).toBe(0);
+
+ wrapper.vm.reviewBySelf();
+
+ expect(mediator.addSelfReview).toHaveBeenCalled();
+ expect(mediator.store.reviewers.length).toBe(1);
+ });
+});
diff --git a/spec/frontend/sidebar/components/severity/severity_spec.js b/spec/frontend/sidebar/components/severity/severity_spec.js
index 2146155791e..99d33e840d5 100644
--- a/spec/frontend/sidebar/components/severity/severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/severity_spec.js
@@ -1,6 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
describe('SeverityToken', () => {
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index bdea33371d8..8f936240b7a 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -3,8 +3,8 @@ import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/components/severity/constants';
-import updateIssuableSeverity from '~/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql';
+import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/constants';
+import updateIssuableSeverity from '~/sidebar/queries/update_issuable_severity.mutation.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
index 2f281cb88f9..5a75299c3a4 100644
--- a/spec/frontend/issuable/bulk_update_sidebar/components/status_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
@@ -1,7 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import StatusDropdown from '~/issuable/bulk_update_sidebar/components/status_dropdown.vue';
-import { statusDropdownOptions } from '~/issuable/bulk_update_sidebar/constants';
+import StatusDropdown from '~/sidebar/components/status/status_dropdown.vue';
+import { statusDropdownOptions } from '~/sidebar/constants';
describe('SubscriptionsDropdown component', () => {
let wrapper;
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
index 56ef7a1ed39..3fb8214606c 100644
--- a/spec/frontend/issuable/bulk_update_sidebar/components/subscriptions_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
@@ -1,8 +1,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import SubscriptionsDropdown from '~/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue';
-import { subscriptionsDropdownOptions } from '~/issuable/bulk_update_sidebar/constants';
+import SubscriptionsDropdown from '~/sidebar/components/subscriptions/subscriptions_dropdown.vue';
+import { subscriptionsDropdownOptions } from '~/sidebar/constants';
describe('SubscriptionsDropdown component', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
index 1a1aa370eef..1a1aa370eef 100644
--- a/spec/frontend/sidebar/subscriptions_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
new file mode 100644
index 00000000000..cb3bb7a4538
--- /dev/null
+++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
@@ -0,0 +1,219 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlAlert, GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import CreateTimelogForm from '~/sidebar/components/time_tracking/create_timelog_form.vue';
+import createTimelogMutation from '~/sidebar/queries/create_timelog.mutation.graphql';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
+
+const mockMutationErrorMessage = 'Example error message';
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ timelogCreate: {
+ errors: [],
+ timelog: {
+ id: 'gid://gitlab/Timelog/1',
+ issue: {},
+ mergeRequest: {},
+ },
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ timelogCreate: {
+ errors: [{ message: mockMutationErrorMessage }],
+ timelog: null,
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue();
+const modalCloseMock = jest.fn();
+
+describe('Create Timelog Form', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findForm = () => wrapper.find('form');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDocsLink = () => wrapper.findByTestId('timetracking-docs-link');
+ const findSaveButton = () => findModal().props('actionPrimary');
+ const findSaveButtonLoadingState = () => findSaveButton().attributes[0].loading;
+ const findSaveButtonDisabledState = () => findSaveButton().attributes[0].disabled;
+
+ const submitForm = () => findForm().trigger('submit');
+
+ const mountComponent = (
+ { props, data, providedProps } = {},
+ mutationResolverMock = rejectedMutationMock,
+ ) => {
+ fakeApollo = createMockApollo([[createTimelogMutation, mutationResolverMock]]);
+
+ wrapper = shallowMountExtended(CreateTimelogForm, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ issuableType: 'issue',
+ ...providedProps,
+ },
+ propsData: {
+ issuableId: '1',
+ ...props,
+ },
+ apolloProvider: fakeApollo,
+ });
+
+ wrapper.vm.$refs.modal.close = modalCloseMock;
+ };
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ describe('save button', () => {
+ it('is disabled and not loading by default', () => {
+ mountComponent();
+
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(true);
+ });
+
+ it('is enabled and not loading when time spent is not empty', () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(false);
+ });
+
+ it('is disabled and loading when the the form is submitted', async () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ submitForm();
+
+ await nextTick();
+
+ expect(findSaveButtonLoadingState()).toBe(true);
+ expect(findSaveButtonDisabledState()).toBe(true);
+ });
+
+ it('is enabled and not loading the when form is submitted but the mutation has errors', async () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(rejectedMutationMock).toHaveBeenCalled();
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(false);
+ });
+
+ it('is enabled and not loading the when form is submitted but the mutation returns errors', async () => {
+ mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithErrorsMock).toHaveBeenCalled();
+ expect(findSaveButtonLoadingState()).toBe(false);
+ expect(findSaveButtonDisabledState()).toBe(false);
+ });
+ });
+
+ describe('form', () => {
+ it('does not call any mutation when the the form is incomplete', async () => {
+ mountComponent();
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(rejectedMutationMock).not.toHaveBeenCalled();
+ });
+
+ it('closes the modal after a successful mutation', async () => {
+ mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithoutErrorsMock);
+
+ submitForm();
+
+ await waitForPromises();
+ await nextTick();
+
+ expect(modalCloseMock).toHaveBeenCalled();
+ });
+
+ it.each`
+ issuableType | typeConstant
+ ${'issue'} | ${TYPE_ISSUE}
+ ${'merge_request'} | ${TYPE_MERGE_REQUEST}
+ `(
+ 'calls the mutation with all the fields when the the form is submitted and issuable type is $issuableType',
+ async ({ issuableType, typeConstant }) => {
+ const timeSpent = '2d';
+ const spentAt = '2022-11-20T21:53:00+0000';
+ const summary = 'Example';
+
+ mountComponent({ data: { timeSpent, spentAt, summary }, providedProps: { issuableType } });
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(rejectedMutationMock).toHaveBeenCalledWith({
+ input: { timeSpent, spentAt, summary, issuableId: convertToGraphQLId(typeConstant, '1') },
+ });
+ },
+ );
+ });
+
+ describe('alert', () => {
+ it('is hidden by default', () => {
+ mountComponent();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('shows an error if the submission fails with a handled error', async () => {
+ mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(mockMutationErrorMessage);
+ });
+
+ it('shows an error if the submission fails with an unhandled error', async () => {
+ mountComponent({ data: { timeSpent: '2d' } });
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe('An error occurred while saving the time entry.');
+ });
+ });
+
+ describe('docs link message', () => {
+ it('is present', () => {
+ mountComponent();
+
+ expect(findDocsLink().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index af72122052f..0259aee48f0 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -8,9 +8,9 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import Report from '~/sidebar/components/time_tracking/report.vue';
-import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
-import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
-import deleteTimelogMutation from '~/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql';
+import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.graphql';
+import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
+import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
import {
getIssueTimelogsQueryResponse,
getMrTimelogsQueryResponse,
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index 835e700e63c..45d8b5e4647 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -268,47 +268,32 @@ describe('Issuable Time Tracker', () => {
});
});
- describe('Help pane', () => {
- const findHelpButton = () => findByTestId('helpButton');
- const findCloseHelpButton = () => findByTestId('closeHelpButton');
-
- beforeEach(async () => {
- wrapper = mountComponent({
- props: {
- initialTimeTracking: {
- timeEstimate: 0,
- totalTimeSpent: 0,
- humanTimeEstimate: '',
- humanTotalTimeSpent: '',
+ describe('Add button', () => {
+ const findAddButton = () => findByTestId('add-time-entry-button');
+
+ it.each`
+ visibility | canAddTimeEntries
+ ${'not visible'} | ${false}
+ ${'visible'} | ${true}
+ `(
+ 'is $visibility when canAddTimeEntries is $canAddTimeEntries',
+ async ({ canAddTimeEntries }) => {
+ wrapper = mountComponent({
+ props: {
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 0,
+ humanTimeEstimate: '',
+ humanTotalTimeSpent: '',
+ },
+ canAddTimeEntries,
},
- },
- });
- await nextTick();
- });
-
- it('should not show the "Help" pane by default', () => {
- expect(findByTestId('helpPane').exists()).toBe(false);
- });
-
- it('should show the "Help" pane when help button is clicked', async () => {
- findHelpButton().trigger('click');
-
- await nextTick();
-
- expect(findByTestId('helpPane').exists()).toBe(true);
- });
-
- it('should not show the "Help" pane when help button is clicked and then closed', async () => {
- findHelpButton().trigger('click');
- await nextTick();
-
- expect(findByTestId('helpPane').exists()).toBe(true);
-
- findCloseHelpButton().trigger('click');
- await nextTick();
+ });
+ await nextTick();
- expect(findByTestId('helpPane').exists()).toBe(false);
- });
+ expect(findAddButton().exists()).toBe(canAddTimeEntries);
+ },
+ );
});
});
diff --git a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
index 846f45345e7..846f45345e7 100644
--- a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
index f73491ca95f..5bfe3b59eb3 100644
--- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import { todosResponse, noTodosResponse } from '../../mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
index 01958a144ed..fb07029a249 100644
--- a/spec/frontend/vue_shared/components/sidebar/todo_button_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
@@ -1,6 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
describe('Todo Button', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
index 8e6597bf80f..8e6597bf80f 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
index 267a467059d..cf9b2828dde 100644
--- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
@@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+import ToggleSidebar from '~/sidebar/components/toggle/toggle_sidebar.vue';
describe('ToggleSidebar', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
index 195cc6ddeeb..6e365df329b 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
@@ -8,7 +8,7 @@ import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown';
-import Mock from './mock_data';
+import Mock from '../mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index bb5e7f7ff16..cdb9ced70b8 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -24,7 +24,8 @@ describe('Sidebar mediator', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
- mock.restore();
+
+ jest.clearAllMocks();
});
it('assigns yourself', () => {
@@ -42,6 +43,52 @@ describe('Sidebar mediator', () => {
});
});
+ it('assigns yourself as a reviewer', () => {
+ mediator.addSelfReview();
+
+ expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
+ expect(mediator.store.reviewers[0]).toEqual(mediatorMockData.currentUser);
+ });
+
+ describe('saves reviewers', () => {
+ const mockUpdateResponseData = {
+ reviewers: [1, 2],
+ assignees: [3, 4],
+ };
+ const field = 'merge_request[reviewers_ids]';
+ const reviewers = [
+ { id: 1, suggested: true },
+ { id: 2, suggested: false },
+ ];
+
+ let serviceSpy;
+
+ beforeEach(() => {
+ mediator.store.reviewers = reviewers;
+ serviceSpy = jest
+ .spyOn(mediator.service, 'update')
+ .mockReturnValue(Promise.resolve({ data: mockUpdateResponseData }));
+ });
+
+ it('sends correct data to service', () => {
+ const data = {
+ reviewer_ids: [1, 2],
+ suggested_reviewer_ids: [1],
+ };
+
+ mediator.saveReviewers(field);
+
+ expect(serviceSpy).toHaveBeenCalledWith(field, data);
+ });
+
+ it('saves reviewers', () => {
+ return mediator.saveReviewers(field).then(() => {
+ expect(mediator.store.assignees).toEqual(mockUpdateResponseData.assignees);
+ expect(mediator.store.reviewers).toEqual(mockUpdateResponseData.reviewers);
+ });
+ });
+ });
+
it('fetches the data', async () => {
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
@@ -49,7 +96,6 @@ describe('Sidebar mediator', () => {
await mediator.fetch();
expect(spy).toHaveBeenCalledWith(mockData);
- spy.mockRestore();
});
it('processes fetched data', () => {
@@ -70,8 +116,6 @@ describe('Sidebar mediator', () => {
mediator.setMoveToProjectId(projectId);
expect(spy).toHaveBeenCalledWith(projectId);
-
- spy.mockRestore();
});
it('fetches autocomplete projects', () => {
@@ -87,9 +131,6 @@ describe('Sidebar mediator', () => {
return mediator.fetchAutocompleteProjects(searchTerm).then(() => {
expect(getterSpy).toHaveBeenCalledWith(searchTerm);
expect(setterSpy).toHaveBeenCalled();
-
- getterSpy.mockRestore();
- setterSpy.mockRestore();
});
});
@@ -106,9 +147,6 @@ describe('Sidebar mediator', () => {
return mediator.moveIssue().then(() => {
expect(moveIssueSpy).toHaveBeenCalledWith(moveToProjectId);
expect(urlSpy).toHaveBeenCalledWith(mockData.web_url);
-
- moveIssueSpy.mockRestore();
- urlSpy.mockRestore();
});
});
});
diff --git a/spec/frontend/sidebar/sidebar_store_spec.js b/spec/frontend/sidebar/stores/sidebar_store_spec.js
index 3930dabfcfa..3f4b80409c2 100644
--- a/spec/frontend/sidebar/sidebar_store_spec.js
+++ b/spec/frontend/sidebar/stores/sidebar_store_spec.js
@@ -1,6 +1,6 @@
import UsersMockHelper from 'helpers/user_mock_data_helper';
import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
+import Mock from '../mock_data';
const ASSIGNEE = {
id: 2,
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index f1dbc004da8..ce1c126f868 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { merge } from 'lodash';
import { GlIntersectionObserver } from '@gitlab/ui';
import { nextTick } from 'vue';
@@ -7,13 +6,14 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import TermsApp from '~/terms/components/app.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
jest.mock('~/lib/utils/common_utils');
+jest.mock('~/behaviors/markdown/render_gfm');
describe('TermsApp', () => {
let wrapper;
- let renderGFMSpy;
const defaultProvide = {
terms: 'foo bar',
@@ -35,7 +35,6 @@ describe('TermsApp', () => {
};
beforeEach(() => {
- renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
isLoggedIn.mockReturnValue(true);
});
@@ -65,7 +64,7 @@ describe('TermsApp', () => {
createComponent();
expect(wrapper.findByText(defaultProvide.terms).exists()).toBe(true);
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
describe('accept button', () => {
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
index dbdff899bac..911bb8878da 100644
--- a/spec/frontend/terraform/components/init_command_modal_spec.js
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -7,12 +7,13 @@ const accessTokensPath = '/path/to/access-tokens-page';
const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
-const stateName = 'production';
+const stateName = 'aws/eu-central-1';
+const stateNameEncoded = encodeURIComponent(stateName);
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
terraform init \\
- -backend-config="address=${terraformApiUrl}/${stateName}" \\
- -backend-config="lock_address=${terraformApiUrl}/${stateName}/lock" \\
- -backend-config="unlock_address=${terraformApiUrl}/${stateName}/lock" \\
+ -backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\
+ -backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="username=${username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
@@ -61,9 +62,15 @@ describe('InitCommandModal', () => {
expect(findLink().attributes('href')).toBe(accessTokensPath);
});
- it('renders the init command with the username and state name prepopulated', () => {
- expect(findInitCommand().text()).toContain(username);
- expect(findInitCommand().text()).toContain(stateName);
+ describe('init command', () => {
+ it('includes correct address', () => {
+ expect(findInitCommand().text()).toContain(
+ `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
+ );
+ });
+ it('includes correct username', () => {
+ expect(findInitCommand().text()).toContain(`-backend-config="username=${username}"`);
+ });
});
it('renders the copyToClipboard button', () => {
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 2eed1e30d0d..0c8ba266201 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -68,6 +68,19 @@ export const removeProjectSuccess = {
},
};
+export const updateScopeSuccess = {
+ data: {
+ ciCdSettingsUpdate: {
+ ciCdSettings: {
+ jobTokenScopeEnabled: false,
+ __typename: 'ProjectCiCdSetting',
+ },
+ errors: [],
+ __typename: 'CiCdSettingsUpdatePayload',
+ },
+ },
+};
+
export const mockProjects = [
{
id: '1',
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
index ea1d9db515a..6fe94e28548 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -8,6 +8,7 @@ import { createAlert } from '~/flash';
import TokenAccess from '~/token_access/components/token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
+import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql';
import getCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_ci_job_token_scope.query.graphql';
import getProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql';
import {
@@ -16,6 +17,7 @@ import {
projectsWithScope,
addProjectSuccess,
removeProjectSuccess,
+ updateScopeSuccess,
} from './mock_data';
const projectPath = 'root/my-repo';
@@ -31,11 +33,11 @@ describe('TokenAccess component', () => {
const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope);
const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope);
- const getProjectsWithScope = jest.fn().mockResolvedValue(projectsWithScope);
+ const getProjectsWithScopeHandler = jest.fn().mockResolvedValue(projectsWithScope);
const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess);
- const addProjectFailureHandler = jest.fn().mockRejectedValue(error);
const removeProjectSuccessHandler = jest.fn().mockResolvedValue(removeProjectSuccess);
- const removeProjectFailureHandler = jest.fn().mockRejectedValue(error);
+ const updateScopeSuccessHandler = jest.fn().mockResolvedValue(updateScopeSuccess);
+ const failureHandler = jest.fn().mockRejectedValue(error);
const findToggle = () => wrapper.findComponent(GlToggle);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -69,7 +71,7 @@ describe('TokenAccess component', () => {
it('shows loading state while waiting on query to resolve', async () => {
createComponent([
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
]);
expect(findLoadingIcon().exists()).toBe(true);
@@ -80,11 +82,53 @@ describe('TokenAccess component', () => {
});
});
+ describe('fetching projects and scope', () => {
+ it('fetches projects and scope correctly', () => {
+ const expectedVariables = {
+ fullPath: 'root/my-repo',
+ };
+
+ createComponent([
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ]);
+
+ expect(enabledJobTokenScopeHandler).toHaveBeenCalledWith(expectedVariables);
+ expect(getProjectsWithScopeHandler).toHaveBeenCalledWith(expectedVariables);
+ });
+
+ it('handles fetch projects error correctly', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, failureHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the projects',
+ });
+ });
+
+ it('handles fetch scope error correctly', async () => {
+ createComponent([
+ [getCIJobTokenScopeQuery, failureHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the job token scope value',
+ });
+ });
+ });
+
describe('toggle', () => {
it('the toggle is on and the alert is hidden', async () => {
createComponent([
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
]);
await waitForPromises();
@@ -96,7 +140,7 @@ describe('TokenAccess component', () => {
it('the toggle is off and the alert is visible', async () => {
createComponent([
[getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
]);
await waitForPromises();
@@ -104,6 +148,47 @@ describe('TokenAccess component', () => {
expect(findToggle().props('value')).toBe(false);
expect(findTokenDisabledAlert().exists()).toBe(true);
});
+
+ describe('update ci job token scope', () => {
+ it('calls updateCIJobTokenScopeMutation mutation', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [updateCIJobTokenScopeMutation, updateScopeSuccessHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', false);
+
+ expect(updateScopeSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ fullPath: 'root/my-repo',
+ jobTokenScopeEnabled: false,
+ },
+ });
+ });
+
+ it('handles update scope error correctly', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [updateCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', true);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
});
describe('add project', () => {
@@ -111,7 +196,7 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
[addProjectCIJobTokenScopeMutation, addProjectSuccessHandler],
],
mountExtended,
@@ -133,8 +218,8 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
- [addProjectCIJobTokenScopeMutation, addProjectFailureHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ [addProjectCIJobTokenScopeMutation, failureHandler],
],
mountExtended,
);
@@ -154,7 +239,7 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
[removeProjectCIJobTokenScopeMutation, removeProjectSuccessHandler],
],
mountExtended,
@@ -176,8 +261,8 @@ describe('TokenAccess component', () => {
createComponent(
[
[getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope],
- [removeProjectCIJobTokenScopeMutation, removeProjectFailureHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ [removeProjectCIJobTokenScopeMutation, failureHandler],
],
mountExtended,
);
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
index bd40a968392..4077564486c 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -2,7 +2,7 @@
exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
<div
- class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
+ class="mr-widget-body media gl-display-flex gl-align-items-center"
>
<div
class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
@@ -61,7 +61,7 @@ exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
class="gl-display-flex gl-align-items-flex-start"
>
<div
- class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
+ class="dropdown b-dropdown gl-dropdown gl-display-block gl-md-display-none! btn-group"
lazy=""
no-caret=""
title="Options"
@@ -87,7 +87,7 @@ exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
</svg>
<span
- class="gl-new-dropdown-button-text gl-sr-only"
+ class="gl-dropdown-button-text gl-sr-only"
>
</span>
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
index 06ee017dee7..270a37f87e7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
@@ -1,9 +1,28 @@
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import api from '~/api';
+
+import showGlobalToast from '~/vue_shared/plugins/global_toast';
+
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+import Actions from '~/vue_merge_request_widget/components/action_buttons.vue';
+
+import { MR_WIDGET_CLOSED_REOPEN_FAILURE } from '~/vue_merge_request_widget/i18n';
+
+jest.mock('~/api', () => ({
+ updateMergeRequest: jest.fn(),
+}));
+jest.mock('~/vue_shared/plugins/global_toast');
+
+useMockLocationHelper();
const MOCK_DATA = {
+ iid: 1,
metrics: {
mergedBy: {},
closedBy: {
@@ -19,22 +38,39 @@ const MOCK_DATA = {
},
targetBranchPath: '/twitter/flight/commits/so_long_jquery',
targetBranch: 'so_long_jquery',
+ targetProjectId: 'twitter/flight',
};
+function createComponent({ shallow = true, props = {} } = {}) {
+ const mounter = shallow ? shallowMount : mount;
+
+ return mounter(closedComponent, {
+ propsData: {
+ mr: MOCK_DATA,
+ ...props,
+ },
+ });
+}
+
+function findActions(wrapper) {
+ return wrapper.findComponent(StateContainer).findComponent(Actions);
+}
+
+function findReopenActionButton(wrapper) {
+ return findActions(wrapper).find('button[data-testid="extension-actions-reopen-button"]');
+}
+
describe('MRWidgetClosed', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallowMount(closedComponent, {
- propsData: {
- mr: MOCK_DATA,
- },
- });
+ wrapper = createComponent();
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
+ if (wrapper) {
+ wrapper.destroy();
+ }
});
it('renders closed icon', () => {
@@ -51,4 +87,93 @@ describe('MRWidgetClosed', () => {
dateReadable: MOCK_DATA.metrics.readableClosedAt,
});
});
+
+ describe('actions', () => {
+ describe('reopen', () => {
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
+ api.updateMergeRequest.mockResolvedValue(true);
+ wrapper = createComponent({ shallow: false });
+ });
+
+ it('shows the "reopen" button', () => {
+ expect(wrapper.findComponent(StateContainer).props().actions.length).toBe(1);
+ expect(findReopenActionButton(wrapper).text()).toBe('Reopen');
+ });
+
+ it('does not show widget actions when the user is not logged in', () => {
+ window.gon = {};
+
+ wrapper = createComponent();
+
+ expect(findActions(wrapper).exists()).toBe(false);
+ });
+
+ it('makes the reopen request with the correct MR information', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ reopenButton.trigger('click');
+ await nextTick();
+
+ expect(api.updateMergeRequest).toHaveBeenCalledWith(
+ MOCK_DATA.targetProjectId,
+ MOCK_DATA.iid,
+ { state_event: 'reopen' },
+ );
+ });
+
+ it('shows "Reopening..." while the reopen network request is pending', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ api.updateMergeRequest.mockReturnValue(new Promise(() => {}));
+
+ reopenButton.trigger('click');
+ await nextTick();
+
+ expect(reopenButton.text()).toBe('Reopening...');
+ });
+
+ it('shows "Refreshing..." when the reopen has succeeded', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(reopenButton.text()).toBe('Refreshing...');
+ });
+
+ it('reloads the page when a reopen has succeeded', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows "Reopen" when a reopen request has failed', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ api.updateMergeRequest.mockRejectedValue(false);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(window.location.reload).not.toHaveBeenCalled();
+ expect(reopenButton.text()).toBe('Reopen');
+ });
+
+ it('requests a toast popup when a reopen request has failed', async () => {
+ const reopenButton = findReopenActionButton(wrapper);
+
+ api.updateMergeRequest.mockRejectedValue(false);
+
+ reopenButton.trigger('click');
+ await waitForPromises();
+
+ expect(showGlobalToast).toHaveBeenCalledTimes(1);
+ expect(showGlobalToast).toHaveBeenCalledWith(MR_WIDGET_CLOSED_REOPEN_FAILURE);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 407bd60b2b7..d34fc0c1e61 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -812,14 +812,30 @@ describe('ReadyToMerge', () => {
);
});
- it('shows the diverged commits text when the source branch is behind the target', () => {
- createComponent({
- mr: { divergedCommitsCount: 9001, userPermissions: { canMerge: false }, canMerge: false },
+ describe('shows the diverged commits text when the source branch is behind the target', () => {
+ it('when the MR can be merged', () => {
+ createComponent({
+ mr: { divergedCommitsCount: 9001 },
+ });
+
+ expect(wrapper.text()).toEqual(
+ expect.stringContaining('The source branch is 9001 commits behind the target branch'),
+ );
});
- expect(wrapper.text()).toEqual(
- expect.stringContaining('The source branch is 9001 commits behind the target branch'),
- );
+ it('when the MR cannot be merged', () => {
+ createComponent({
+ mr: {
+ divergedCommitsCount: 9001,
+ userPermissions: { canMerge: false },
+ canMerge: false,
+ },
+ });
+
+ expect(wrapper.text()).toEqual(
+ expect.stringContaining('The source branch is 9001 commits behind the target branch'),
+ );
+ });
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
new file mode 100644
index 00000000000..366ea113162
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
@@ -0,0 +1,47 @@
+import { GlButton, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Actions from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = shallowMount(Actions, {
+ propsData: { ...propsData, widget: 'test' },
+ });
+}
+
+describe('~/vue_merge_request_widget/components/widget/action_buttons.vue', () => {
+ 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('calls action click handler', async () => {
+ const onClick = jest.fn();
+
+ factory({
+ tertiaryButtons: [{ text: 'hello world', onClick }],
+ });
+
+ await wrapper.findComponent(GlButton).vm.$emit('click');
+
+ expect(onClick).toHaveBeenCalled();
+ });
+
+ it('renders tertiary actions in dropdown', () => {
+ factory({
+ tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
+ });
+
+ expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
index e4bee6b8652..791fe541eb6 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue';
-import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
+import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', () => {
@@ -76,7 +76,10 @@ describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue',
},
});
- expect(findHelpPopover().props('options')).toEqual({ title: 'Help popover title' });
+ const popover = findHelpPopover();
+
+ expect(popover.props('options')).toEqual({ title: 'Help popover title' });
+ expect(popover.props('icon')).toBe('information-o');
expect(wrapper.findByText('Help popover content').exists()).toBe(true);
expect(wrapper.findByText('Learn more').attributes('href')).toBe('/path/to/docs');
expect(wrapper.findByText('Learn more').attributes('target')).toBe('_blank');
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 9635e050e4d..4c93c88de16 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
-import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
+import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
@@ -24,6 +24,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const findActionButtons = () => wrapper.findComponent(ActionButtons);
const findToggleButton = () => wrapper.findByTestId('toggle-button');
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
+ const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller');
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(Widget, {
@@ -212,7 +213,10 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
},
});
- expect(findHelpPopover().props('options')).toEqual({ title: 'My help popover title' });
+ const popover = findHelpPopover();
+
+ expect(popover.props('options')).toEqual({ title: 'My help popover title' });
+ expect(popover.props('icon')).toBe('information-o');
expect(wrapper.findByText('Help popover content').exists()).toBe(true);
expect(wrapper.findByText('Learn more').attributes('href')).toBe('/path/to/docs');
expect(wrapper.findByText('Learn more').attributes('target')).toBe('_blank');
@@ -370,7 +374,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
href: '#',
target: '_blank',
id: 'full-report-button',
- text: 'Full Report',
+ text: 'Full report',
},
],
},
@@ -388,7 +392,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('when full report is clicked it should call the respective telemetry event', async () => {
expect(wrapper.vm.telemetryHub.fullReportClicked).not.toHaveBeenCalled();
- wrapper.findByText('Full Report').vm.$emit('click');
+ wrapper.findByText('Full report').vm.$emit('click');
await nextTick();
expect(wrapper.vm.telemetryHub.fullReportClicked).toHaveBeenCalledTimes(1);
});
@@ -408,4 +412,30 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(wrapper.vm.telemetryHub).toBe(null);
});
});
+
+ describe('dynamic content', () => {
+ const content = [
+ {
+ id: 'row-id',
+ header: ['This is a header', 'This is a subheader'],
+ text: 'Main text for the row',
+ subtext: 'Optional: Smaller sub-text to be displayed below the main text',
+ },
+ ];
+
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ content,
+ },
+ });
+ });
+
+ it('uses a dynamic scroller to show the items', async () => {
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+ expect(findDynamicScroller().props('items')).toEqual(content);
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
index 05df66165dd..baef247b649 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
@@ -8,17 +8,17 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
-import { failedReport } from 'jest/reports/mock_data/mock_data';
-import mixedResultsTestReports from 'jest/reports/mock_data/new_and_fixed_failures_report.json';
-import newErrorsTestReports from 'jest/reports/mock_data/new_errors_report.json';
-import newFailedTestReports from 'jest/reports/mock_data/new_failures_report.json';
-import newFailedTestWithNullFilesReport from 'jest/reports/mock_data/new_failures_with_null_files_report.json';
-import successTestReports from 'jest/reports/mock_data/no_failures_report.json';
-import resolvedFailures from 'jest/reports/mock_data/resolved_failures.json';
-import recentFailures from 'jest/reports/mock_data/recent_failures_report.json';
+import { failedReport } from 'jest/ci/reports/mock_data/mock_data';
+import mixedResultsTestReports from 'jest/ci/reports/mock_data/new_and_fixed_failures_report.json';
+import newErrorsTestReports from 'jest/ci/reports/mock_data/new_errors_report.json';
+import newFailedTestReports from 'jest/ci/reports/mock_data/new_failures_report.json';
+import newFailedTestWithNullFilesReport from 'jest/ci/reports/mock_data/new_failures_with_null_files_report.json';
+import successTestReports from 'jest/ci/reports/mock_data/no_failures_report.json';
+import resolvedFailures from 'jest/ci/reports/mock_data/resolved_failures.json';
+import recentFailures from 'jest/ci/reports/mock_data/recent_failures_report.json';
const reportWithParsingErrors = failedReport;
reportWithParsingErrors.suites[0].suite_errors = {
@@ -82,7 +82,7 @@ describe('Test report extension', () => {
});
it('with a 204 response, continues to display loading state', async () => {
- mockApi(httpStatusCodes.NO_CONTENT, '');
+ mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index 9a72e4a086b..f0ebbb1a82e 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -6,10 +7,10 @@ import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality';
-import httpStatusCodes from '~/lib/utils/http_status';
+import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import { i18n } from '~/vue_merge_request_widget/extensions/code_quality/constants';
import {
codeQualityResponseNewErrors,
- codeQualityResponseResolvedErrors,
codeQualityResponseResolvedAndNewErrors,
codeQualityResponseNoErrors,
} from './mock_data';
@@ -58,46 +59,55 @@ describe('Code Quality extension', () => {
createComponent();
- expect(wrapper.text()).toBe('Code Quality test metrics results are being parsed');
+ expect(wrapper.text()).toBe(i18n.loading);
});
- it('displays failed loading text', async () => {
- mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
-
+ it('with a 204 response, continues to display loading state', async () => {
+ mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
await waitForPromises();
- expect(wrapper.text()).toBe('Code Quality failed loading results');
+
+ expect(wrapper.text()).toBe(i18n.loading);
});
- it('displays quality degradation', async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
+ it('displays failed loading text', async () => {
+ mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
-
- expect(wrapper.text()).toBe('Code Quality degraded on 2 points.');
+ expect(wrapper.text()).toBe(i18n.error);
});
- it('displays quality improvement', async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseResolvedErrors);
+ it('displays correct single Report', async () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
createComponent();
await waitForPromises();
- expect(wrapper.text()).toBe('Code Quality improved on 2 points.');
+ expect(wrapper.text()).toBe(
+ i18n.degradedCopy(i18n.singularReport(codeQualityResponseNewErrors.new_errors)),
+ );
});
it('displays quality improvement and degradation', async () => {
mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors);
createComponent();
-
await waitForPromises();
- expect(wrapper.text()).toBe('Code Quality improved on 1 point and degraded on 1 point.');
+ // replacing strong tags because they will not be found in the rendered text
+ expect(wrapper.text()).toBe(
+ i18n
+ .improvementAndDegradationCopy(
+ i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.resolved_errors),
+ i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.new_errors),
+ )
+ .replace(/%{strong_start}/g, '')
+ .replace(/%{strong_end}/g, ''),
+ );
});
it('displays no detected errors', async () => {
@@ -107,7 +117,7 @@ describe('Code Quality extension', () => {
await waitForPromises();
- expect(wrapper.text()).toBe('No changes to Code Quality.');
+ expect(wrapper.text()).toBe(i18n.noChanges);
});
});
@@ -138,8 +148,17 @@ describe('Code Quality extension', () => {
"Minor - Parsing error: 'return' outside of function in index.js:12",
);
expect(text.resolvedError).toContain(
- "Minor - Parsing error: 'return' outside of function in index.js:12",
+ "Minor - Parsing error: 'return' outside of function Fixed in index.js:12",
);
});
+
+ it('adds fixed indicator (badge) when error is resolved', () => {
+ expect(findAllExtensionListItems().at(1).findComponent(GlBadge).exists()).toBe(true);
+ expect(findAllExtensionListItems().at(1).findComponent(GlBadge).text()).toEqual(i18n.fixed);
+ });
+
+ it('should not add fixed indicator (badge) when error is new', () => {
+ expect(findAllExtensionListItems().at(0).findComponent(GlBadge).exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
index f5ad0ce7377..2e8e70f25db 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
@@ -23,31 +23,6 @@ export const codeQualityResponseNewErrors = {
},
};
-export const codeQualityResponseResolvedErrors = {
- status: 'failed',
- new_errors: [],
- resolved_errors: [
- {
- description: "Parsing error: 'return' outside of function",
- severity: 'minor',
- file_path: 'index.js',
- line: 12,
- },
- {
- description: 'TODO found',
- severity: 'minor',
- file_path: '.gitlab-ci.yml',
- line: 73,
- },
- ],
- existing_errors: [],
- summary: {
- total: 2,
- resolved: 2,
- errored: 0,
- },
-};
-
export const codeQualityResponseResolvedAndNewErrors = {
status: 'failed',
new_errors: [
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 0f4637d18d9..683858b331d 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -22,6 +22,7 @@ import {
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
+import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
@@ -62,6 +63,7 @@ describe('MrWidgetOptions', () => {
let mock;
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
+ const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
const findExtensionToggleButton = () =>
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
const findExtensionLink = (linkHref) =>
@@ -1228,5 +1230,22 @@ describe('MrWidgetOptions', () => {
expect(api.trackRedisCounterEvent).not.toHaveBeenCalled();
});
});
+
+ describe('widget container', () => {
+ afterEach(() => {
+ delete window.gon.features.refactorSecurityExtension;
+ });
+
+ it('should not be displayed when the refactor_security_extension feature flag is turned off', () => {
+ createComponent();
+ expect(findWidgetContainer().exists()).toBe(false);
+ });
+
+ it('should be displayed when the refactor_security_extension feature flag is turned on', () => {
+ window.gon.features.refactorSecurityExtension = true;
+ createComponent();
+ expect(findWidgetContainer().exists()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
deleted file mode 100644
index bdf5ea23812..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ /dev/null
@@ -1,305 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
-<div
- class="awards js-awards-block"
->
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
- data-testid="award-button"
- title="Ada, Leonardo, and Marie reacted with :thumbsup:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="thumbsup"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 3
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You, Ada, and Marie reacted with :thumbsdown:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="thumbsdown"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 3
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
- data-testid="award-button"
- title="Ada and Jane reacted with :smile:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="smile"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 2
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You, Ada, Jane, and Leonardo reacted with :ok_hand:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="ok_hand"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 4
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You reacted with :cactus:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="cactus"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 1
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
- data-testid="award-button"
- title="Marie reacted with :a:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="a"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 1
- </span>
- </span>
- </button>
- <button
- class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
- data-testid="award-button"
- title="You reacted with :b:"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="award-emoji-block"
- data-testid="award-html"
- >
- <gl-emoji
- data-name="b"
- />
- </span>
-
- <span
- class="gl-button-text"
- >
-
- <span
- class="js-counter"
- >
- 1
- </span>
- </span>
- </button>
-
- <div
- class="award-menu-holder gl-my-2"
- >
- <div
- class="emoji-picker"
- data-testid="emoji-picker"
- title="Add reaction"
- >
- <div
- boundary="scrollParent"
- class="dropdown b-dropdown gl-new-dropdown btn-group"
- id="__BVID__13"
- lazy=""
- menu-class="dropdown-extended-height"
- no-flip=""
- >
- <!---->
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary"
- id="__BVID__13__BV_toggle_"
- type="button"
- >
- <span
- class="gl-sr-only"
- >
- Add reaction
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-neutral"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="slight-smile-icon"
- role="img"
- >
- <use
- href="#slight-smile"
- />
- </svg>
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-positive"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="smiley-icon"
- role="img"
- >
- <use
- href="#smiley"
- />
- </svg>
- </span>
-
- <span
- class="reaction-control-icon reaction-control-icon-super-positive"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="smile-icon"
- role="img"
- >
- <use
- href="#smile"
- />
- </svg>
- </span>
- </button>
- <ul
- aria-labelledby="__BVID__13__BV_toggle_"
- class="dropdown-menu dropdown-extended-height dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
- </div>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
index 87eaabf4e98..b7b43264330 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
@@ -7,6 +7,7 @@ exports[`MemoryGraph Render chart should draw container with chart 1`] = `
>
<gl-sparkline-chart-stub
data="Nov 12 2019 19:17:33,2.87,Nov 12 2019 19:18:33,2.78,Nov 12 2019 19:19:33,2.78,Nov 12 2019 19:20:33,3.01"
+ gradient=""
height="25"
tooltiplabel="MB"
/>
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index 07c53c04723..f3fb840b270 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -1,6 +1,5 @@
-import { GlDropdown, GlDropdownDivider, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownDivider, GlButton, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
const TEST_ACTION = {
@@ -32,7 +31,6 @@ describe('Actions button component', () => {
function createComponent(props) {
wrapper = shallowMount(ActionsButton, {
propsData: { ...props },
- directives: { GlTooltip: createMockDirective() },
});
}
@@ -40,15 +38,9 @@ describe('Actions button component', () => {
wrapper.destroy();
});
- const getTooltip = (child) => {
- const directiveBinding = getBinding(child.element, 'gl-tooltip');
-
- return directiveBinding.value;
- };
const findButton = () => wrapper.findComponent(GlButton);
- const findButtonTooltip = () => getTooltip(findButton());
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownTooltip = () => getTooltip(findDropdown());
const parseDropdownItems = () =>
findDropdown()
.findAll('gl-dropdown-item-stub,gl-dropdown-divider-stub')
@@ -88,8 +80,8 @@ describe('Actions button component', () => {
expect(findButton().text()).toBe(TEST_ACTION.text);
});
- it('should have tooltip', () => {
- expect(findButtonTooltip()).toBe(TEST_ACTION.tooltip);
+ it('should not have tooltip', () => {
+ expect(findTooltip().exists()).toBe(false);
});
it('should have attrs', () => {
@@ -105,7 +97,18 @@ describe('Actions button component', () => {
it('should have tooltip', () => {
createComponent({ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }] });
- expect(findButtonTooltip()).toBe(TEST_TOOLTIP);
+ expect(findTooltip().text()).toBe(TEST_TOOLTIP);
+ });
+ });
+
+ describe('when showActionTooltip is false', () => {
+ it('should not have tooltip', () => {
+ createComponent({
+ actions: [{ ...TEST_ACTION, tooltip: TEST_TOOLTIP }],
+ showActionTooltip: false,
+ });
+
+ expect(findTooltip().exists()).toBe(false);
});
});
@@ -174,8 +177,8 @@ describe('Actions button component', () => {
expect(wrapper.emitted('select')).toEqual([[TEST_ACTION_2.key]]);
});
- it('should have tooltip value', () => {
- expect(findDropdownTooltip()).toBe(TEST_ACTION.tooltip);
+ it('should not have tooltip value', () => {
+ expect(findTooltip().exists()).toBe(false);
});
});
@@ -199,7 +202,7 @@ describe('Actions button component', () => {
});
it('should have tooltip value', () => {
- expect(findDropdownTooltip()).toBe(TEST_ACTION_2.tooltip);
+ expect(findTooltip().text()).toBe(TEST_ACTION_2.tooltip);
});
});
});
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 1c8cf726aca..c7f9d8fd8d5 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -38,7 +38,18 @@ const TEST_AWARDS = [
createAward(EMOJI_CACTUS, USERS.root),
createAward(EMOJI_A, USERS.marie),
createAward(EMOJI_B, USERS.root),
+ createAward(EMOJI_100, USERS.ada),
];
+const TEST_AWARDS_LENGTH = [
+ EMOJI_SMILE,
+ EMOJI_OK,
+ EMOJI_THUMBSUP,
+ EMOJI_THUMBSDOWN,
+ EMOJI_A,
+ EMOJI_B,
+ EMOJI_CACTUS,
+ EMOJI_100,
+].length;
const TEST_ADD_BUTTON_CLASS = 'js-test-add-button-class';
const REACTION_CONTROL_CLASSES = [
@@ -88,10 +99,6 @@ describe('vue_shared/components/awards_list', () => {
});
});
- it('matches snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
@@ -108,6 +115,12 @@ describe('vue_shared/components/awards_list', () => {
},
{
classes: REACTION_CONTROL_CLASSES,
+ count: 1,
+ html: matchingEmojiTag(EMOJI_100),
+ title: `Ada reacted with :${EMOJI_100}:`,
+ },
+ {
+ classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
@@ -142,33 +155,23 @@ describe('vue_shared/components/awards_list', () => {
it('with award clicked, it emits award', () => {
expect(wrapper.emitted().award).toBeUndefined();
- findAwardButtons().at(2).vm.$emit('click');
+ findAwardButtons().at(3).vm.$emit('click');
expect(wrapper.emitted().award).toEqual([[EMOJI_SMILE]]);
});
- it('shows add award button', () => {
- const btn = findAddAwardButton();
+ it('with numeric award clicked, it emits award as is', () => {
+ expect(wrapper.emitted().award).toBeUndefined();
- expect(btn.exists()).toBe(true);
- });
- });
+ findAwardButtons().at(2).vm.$emit('click');
- describe('with numeric award', () => {
- beforeEach(() => {
- createComponent({
- awards: [createAward(EMOJI_100, USERS.ada)],
- canAwardEmoji: true,
- currentUserId: USERS.root.id,
- });
+ expect(wrapper.emitted().award).toEqual([[EMOJI_100]]);
});
- it('when clicked, it emits award as number', () => {
- expect(wrapper.emitted().award).toBeUndefined();
-
- findAwardButtons().at(0).vm.$emit('click');
+ it('shows add award button', () => {
+ const btn = findAddAwardButton();
- expect(wrapper.emitted().award).toEqual([[Number(EMOJI_100)]]);
+ expect(btn.exists()).toBe(true);
});
});
@@ -210,7 +213,7 @@ describe('vue_shared/components/awards_list', () => {
it('disables award buttons', () => {
const buttons = findAwardButtons();
- expect(buttons.length).toBe(7);
+ expect(buttons.length).toBe(TEST_AWARDS_LENGTH);
expect(buttons.wrappers.every((x) => x.classes('disabled'))).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
index f28805471f8..a37071aec9b 100644
--- a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -1,7 +1,8 @@
import { mount } from '@vue/test-utils';
import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
-import '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('ContentViewer', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 01ef52c6af9..0d329b6a065 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -1,11 +1,12 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('MarkdownViewer', () => {
let wrapper;
let mock;
@@ -26,7 +27,6 @@ describe('MarkdownViewer', () => {
mock = new MockAdapter(axios);
jest.spyOn(axios, 'post');
- jest.spyOn($.fn, 'renderGFM');
});
afterEach(() => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 1b9ca8e6092..b0e393bbf5e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -13,7 +13,10 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import {
FILTERED_SEARCH_TERM,
- SortDirection,
+ SORT_DIRECTION,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -87,7 +90,7 @@ describe('FilteredSearchBarRoot', () => {
it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0]);
- expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
+ expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending);
expect(wrapper.findComponent(GlButtonGroup).exists()).toBe(true);
expect(wrapper.findComponent(GlButton).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
@@ -110,9 +113,9 @@ describe('FilteredSearchBarRoot', () => {
describe('tokenSymbols', () => {
it('returns a map containing type and symbols from `tokens` prop', () => {
expect(wrapper.vm.tokenSymbols).toEqual({
- author_username: '@',
- label_name: '~',
- milestone_title: '%',
+ [TOKEN_TYPE_AUTHOR]: '@',
+ [TOKEN_TYPE_LABEL]: '~',
+ [TOKEN_TYPE_MILESTONE]: '%',
});
});
});
@@ -120,9 +123,9 @@ describe('FilteredSearchBarRoot', () => {
describe('tokenTitles', () => {
it('returns a map containing type and title from `tokens` prop', () => {
expect(wrapper.vm.tokenTitles).toEqual({
- author_username: 'Author',
- label_name: 'Label',
- milestone_title: 'Milestone',
+ [TOKEN_TYPE_AUTHOR]: 'Author',
+ [TOKEN_TYPE_LABEL]: 'Label',
+ [TOKEN_TYPE_MILESTONE]: 'Milestone',
});
});
});
@@ -132,7 +135,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.ascending,
+ selectedSortDirection: SORT_DIRECTION.ascending,
});
expect(wrapper.vm.sortDirectionIcon).toBe('sort-lowest');
@@ -142,7 +145,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
});
expect(wrapper.vm.sortDirectionIcon).toBe('sort-highest');
@@ -154,7 +157,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.ascending,
+ selectedSortDirection: SORT_DIRECTION.ascending,
});
expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Ascending');
@@ -164,7 +167,7 @@ describe('FilteredSearchBarRoot', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
});
expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Descending');
@@ -272,11 +275,11 @@ describe('FilteredSearchBarRoot', () => {
});
it('sets `selectedSortDirection` to be opposite of its current value', () => {
- expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
+ expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.descending);
wrapper.vm.handleSortDirectionClick();
- expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.ascending);
+ expect(wrapper.vm.selectedSortDirection).toBe(SORT_DIRECTION.ascending);
});
it('emits component event `onSort` with opposite of currently selected sort by value', () => {
@@ -384,7 +387,7 @@ describe('FilteredSearchBarRoot', () => {
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
selectedSortOption: mockSortOptions[0],
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
recentSearches: mockHistoryItems,
});
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 a6713b7e7e4..b2f4c780f51 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -1,8 +1,28 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
-import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+import { mockLabels } from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
import Api from '~/api';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONTACT,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_ORGANIZATION,
+ TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_SOURCE_BRANCH,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SOURCE_BRANCH,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@@ -11,7 +31,7 @@ import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/rel
import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue';
import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue';
-export const mockAuthor1 = {
+export const mockUser1 = {
id: 1,
name: 'Administrator',
username: 'root',
@@ -20,7 +40,7 @@ export const mockAuthor1 = {
web_url: 'http://0.0.0.0:3000/root',
};
-export const mockAuthor2 = {
+export const mockUser2 = {
id: 2,
name: 'Claudio Beer',
username: 'ericka_terry',
@@ -29,7 +49,7 @@ export const mockAuthor2 = {
web_url: 'http://0.0.0.0:3000/ericka_terry',
};
-export const mockAuthor3 = {
+export const mockUser3 = {
id: 6,
name: 'Shizue Hartmann',
username: 'junita.weimann',
@@ -38,7 +58,7 @@ export const mockAuthor3 = {
web_url: 'http://0.0.0.0:3000/junita.weimann',
};
-export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
+export const mockUsers = [mockUser1, mockUser2, mockUser3];
export const mockBranches = [{ name: 'Main' }, { name: 'v1.x' }, { name: 'my-Branch' }];
@@ -197,86 +217,86 @@ export const mockEmoji2 = {
export const mockEmojis = [mockEmoji1, mockEmoji2];
export const mockBranchToken = {
- type: 'source_branch',
+ type: TOKEN_TYPE_SOURCE_BRANCH,
icon: 'branch',
- title: 'Source Branch',
+ title: TOKEN_TITLE_SOURCE_BRANCH,
unique: true,
token: BranchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchBranches: Api.branches.bind(Api),
};
export const mockAuthorToken = {
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
icon: 'user',
- title: 'Author',
+ title: TOKEN_TITLE_AUTHOR,
unique: false,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: 'gitlab-org/gitlab-test',
- fetchAuthors: Api.projectUsers.bind(Api),
+ fetchUsers: Api.projectUsers.bind(Api),
};
export const mockLabelToken = {
- type: 'label_name',
+ type: TOKEN_TYPE_LABEL,
icon: 'labels',
- title: 'Label',
+ title: TOKEN_TITLE_LABEL,
unique: false,
symbol: '~',
token: LabelToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchLabels: () => Promise.resolve(mockLabels),
};
export const mockMilestoneToken = {
- type: 'milestone_title',
+ type: TOKEN_TYPE_MILESTONE,
icon: 'clock',
- title: 'Milestone',
+ title: TOKEN_TITLE_MILESTONE,
unique: true,
symbol: '%',
token: MilestoneToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
export const mockReleaseToken = {
- type: 'release',
+ type: TOKEN_TYPE_RELEASE,
icon: 'rocket',
- title: 'Release',
+ title: TOKEN_TITLE_RELEASE,
token: ReleaseToken,
fetchReleases: () => Promise.resolve(),
};
export const mockReactionEmojiToken = {
- type: 'my_reaction_emoji',
+ type: TOKEN_TYPE_MY_REACTION,
icon: 'thumb-up',
- title: 'My-Reaction',
+ title: TOKEN_TITLE_MY_REACTION,
unique: true,
token: EmojiToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchEmojis: () => Promise.resolve(mockEmojis),
};
export const mockCrmContactToken = {
- type: 'crm_contact',
- title: 'Contact',
+ type: TOKEN_TYPE_CONTACT,
+ title: TOKEN_TITLE_CONTACT,
icon: 'user',
token: CrmContactToken,
isProject: false,
fullPath: 'group',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
};
export const mockCrmOrganizationToken = {
- type: 'crm_contact',
- title: 'Organization',
+ type: TOKEN_TYPE_ORGANIZATION,
+ title: TOKEN_TITLE_ORGANIZATION,
icon: 'user',
token: CrmOrganizationToken,
isProject: false,
fullPath: 'group',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
};
@@ -286,7 +306,7 @@ export const mockMembershipToken = {
title: 'Membership',
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ value: 'exclude', title: 'Direct' },
{ value: 'only', title: 'Inherited' },
@@ -301,7 +321,7 @@ export const mockMembershipTokenOptionsWithoutTitles = {
export const mockAvailableTokens = [mockAuthorToken, mockLabelToken, mockMilestoneToken];
export const tokenValueAuthor = {
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
value: {
data: 'root',
operator: '=',
@@ -309,7 +329,7 @@ export const tokenValueAuthor = {
};
export const tokenValueLabel = {
- type: 'label_name',
+ type: TOKEN_TYPE_LABEL,
value: {
operator: '=',
data: 'bug',
@@ -317,7 +337,7 @@ export const tokenValueLabel = {
};
export const tokenValueMilestone = {
- type: 'milestone_title',
+ type: TOKEN_TYPE_MILESTONE,
value: {
operator: '=',
data: 'v1.0',
@@ -333,7 +353,7 @@ export const tokenValueMembership = {
};
export const tokenValueConfidential = {
- type: 'confidential',
+ type: TOKEN_TYPE_CONFIDENTIAL,
value: {
operator: '=',
data: true,
@@ -341,23 +361,10 @@ export const tokenValueConfidential = {
};
export const tokenValuePlain = {
- type: 'filtered-search-term',
+ type: FILTERED_SEARCH_TERM,
value: { data: 'foo' },
};
-export const tokenValueEmpty = {
- type: 'filtered-search-term',
- value: { data: '' },
-};
-
-export const tokenValueEpic = {
- type: 'epic_iid',
- value: {
- operator: '=',
- data: '"foo"::&42',
- },
-};
-
export const mockHistoryItems = [
[tokenValueAuthor, tokenValueLabel, tokenValueMilestone, 'duo'],
[tokenValueAuthor, 'si'],
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 a0126c2bd63..164235e4bb9 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
@@ -12,12 +12,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
mockRegularLabel,
mockLabels,
-} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+} from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
import {
- DEFAULT_NONE_ANY,
+ OPTIONS_NONE_ANY,
OPERATOR_IS,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
getRecentlyUsedSuggestions,
@@ -76,7 +76,7 @@ const mockProps = {
active: false,
suggestions: [],
suggestionsLoading: false,
- defaultSuggestions: DEFAULT_NONE_ANY,
+ defaultSuggestions: OPTIONS_NONE_ANY,
getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data),
cursorPosition: 'start',
};
@@ -301,13 +301,13 @@ describe('BaseToken', () => {
describe('with default suggestions', () => {
describe.each`
- operator | shouldRenderFilteredSearchSuggestion
- ${OPERATOR_IS} | ${true}
- ${OPERATOR_IS_NOT} | ${false}
+ operator | shouldRenderFilteredSearchSuggestion
+ ${OPERATOR_IS} | ${true}
+ ${OPERATOR_NOT} | ${false}
`('when operator is $operator', ({ shouldRenderFilteredSearchSuggestion, operator }) => {
beforeEach(() => {
const props = {
- defaultSuggestions: DEFAULT_NONE_ANY,
+ defaultSuggestions: OPTIONS_NONE_ANY,
value: { data: '', operator },
};
@@ -322,7 +322,7 @@ describe('BaseToken', () => {
if (shouldRenderFilteredSearchSuggestion) {
expect(filteredSearchSuggestions.map((c) => c.props())).toMatchObject(
- DEFAULT_NONE_ANY.map((opt) => ({ value: opt.value })),
+ OPTIONS_NONE_ANY.map((opt) => ({ value: opt.value })),
);
} else {
expect(filteredSearchSuggestions).toHaveLength(0);
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 05b42011fe1..311d5a13280 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
@@ -11,7 +11,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import { mockBranches, mockBranchToken } from '../mock_data';
@@ -112,7 +112,7 @@ describe('BranchToken', () => {
});
describe('template', () => {
- const defaultBranches = DEFAULT_NONE_ANY;
+ const defaultBranches = OPTIONS_NONE_ANY;
async function showSuggestions() {
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index 5b744521979..7be7035a0f2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -10,7 +10,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import CrmContactToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue';
import searchCrmContactsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_contacts.query.graphql';
@@ -187,7 +187,7 @@ describe('CrmContactToken', () => {
});
describe('template', () => {
- const defaultContacts = DEFAULT_NONE_ANY;
+ const defaultContacts = OPTIONS_NONE_ANY;
it('renders base-token component', () => {
mountComponent({
@@ -250,7 +250,7 @@ describe('CrmContactToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', () => {
mountComponent({
active: true,
config: { ...mockCrmContactToken },
@@ -262,8 +262,8 @@ describe('CrmContactToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
- DEFAULT_NONE_ANY.forEach((contact, index) => {
+ expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
+ OPTIONS_NONE_ANY.forEach((contact, index) => {
expect(suggestions.at(index).text()).toBe(contact.text);
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index 3a3e96032e8..ecd3e8a04f1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -10,7 +10,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import CrmOrganizationToken from '~/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue';
import searchCrmOrganizationsQuery from '~/vue_shared/components/filtered_search_bar/queries/search_crm_organizations.query.graphql';
@@ -186,7 +186,7 @@ describe('CrmOrganizationToken', () => {
});
describe('template', () => {
- const defaultOrganizations = DEFAULT_NONE_ANY;
+ const defaultOrganizations = OPTIONS_NONE_ANY;
it('renders base-token component', () => {
mountComponent({
@@ -249,7 +249,7 @@ describe('CrmOrganizationToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', () => {
mountComponent({
active: true,
config: { ...mockCrmOrganizationToken },
@@ -261,8 +261,8 @@ describe('CrmOrganizationToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
- DEFAULT_NONE_ANY.forEach((organization, index) => {
+ expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
+ OPTIONS_NONE_ANY.forEach((organization, index) => {
expect(suggestions.at(index).text()).toBe(organization.text);
});
});
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 e8436d2db17..773df01ada7 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
@@ -12,9 +12,9 @@ import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
- DEFAULT_LABEL_NONE,
- DEFAULT_LABEL_ANY,
- DEFAULT_NONE_ANY,
+ OPTION_NONE,
+ OPTION_ANY,
+ OPTIONS_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
@@ -118,7 +118,7 @@ describe('EmojiToken', () => {
});
describe('template', () => {
- const defaultEmojis = DEFAULT_NONE_ANY;
+ const defaultEmojis = OPTIONS_NONE_ANY;
beforeEach(async () => {
wrapper = createComponent({
@@ -181,7 +181,7 @@ describe('EmojiToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_LABEL_NONE` and `DEFAULT_LABEL_ANY` as default suggestions', async () => {
+ it('renders `OPTION_NONE` and `OPTION_ANY` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockReactionEmojiToken },
@@ -195,8 +195,8 @@ describe('EmojiToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(2);
- expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_NONE.text);
- expect(suggestions.at(1).text()).toBe(DEFAULT_LABEL_ANY.text);
+ expect(suggestions.at(0).text()).toBe(OPTION_NONE.text);
+ expect(suggestions.at(1).text()).toBe(OPTION_ANY.text);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 8ca12afacec..9d96123c17f 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
@@ -10,11 +10,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import {
mockRegularLabel,
mockLabels,
-} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+} from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@@ -141,7 +141,7 @@ describe('LabelToken', () => {
});
describe('template', () => {
- const defaultLabels = DEFAULT_NONE_ANY;
+ const defaultLabels = OPTIONS_NONE_ANY;
beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
@@ -209,7 +209,7 @@ describe('LabelToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', () => {
wrapper = createComponent({
active: true,
config: { ...mockLabelToken },
@@ -221,8 +221,8 @@ describe('LabelToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
- DEFAULT_NONE_ANY.forEach((label, index) => {
+ expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
+ OPTIONS_NONE_ANY.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
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/user_token_spec.js
index 5371b9af475..32cb74d5f80 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/user_token_spec.js
@@ -11,11 +11,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { mockAuthorToken, mockAuthors } from '../mock_data';
+import { mockAuthorToken, mockUsers } from '../mock_data';
jest.mock('~/flash');
const defaultStubs = {
@@ -28,7 +28,7 @@ const defaultStubs = {
},
};
-const mockPreloadedAuthors = [
+const mockPreloadedUsers = [
{
id: 13,
name: 'Administrator',
@@ -46,7 +46,7 @@ function createComponent(options = {}) {
data = {},
listeners = {},
} = options;
- return mount(AuthorToken, {
+ return mount(UserToken, {
propsData: {
config,
value,
@@ -66,7 +66,7 @@ function createComponent(options = {}) {
});
}
-describe('AuthorToken', () => {
+describe('UserToken', () => {
const originalGon = window.gon;
const currentUserLength = 1;
let mock;
@@ -85,40 +85,40 @@ describe('AuthorToken', () => {
});
describe('methods', () => {
- describe('fetchAuthors', () => {
+ describe('fetchUsers', () => {
beforeEach(() => {
wrapper = createComponent();
});
- it('calls `config.fetchAuthors` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors');
+ it('calls `config.fetchUsers` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchUsers');
- getBaseToken().vm.$emit('fetch-suggestions', mockAuthors[0].username);
+ getBaseToken().vm.$emit('fetch-suggestions', mockUsers[0].username);
- expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith(
+ expect(wrapper.vm.config.fetchUsers).toHaveBeenCalledWith(
mockAuthorToken.fetchPath,
- mockAuthors[0].username,
+ mockUsers[0].username,
);
});
- it('sets response to `authors` when request is succesful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors);
+ it('sets response to `users` when request is successful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchUsers').mockResolvedValue(mockUsers);
getBaseToken().vm.$emit('fetch-suggestions', 'root');
return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
// TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
describe('when there are null users presents', () => {
- const mockAuthorsWithNullUser = mockAuthors.concat([null]);
+ const mockUsersWithNullUser = mockUsers.concat([null]);
beforeEach(() => {
jest
- .spyOn(wrapper.vm.config, 'fetchAuthors')
- .mockResolvedValue({ data: mockAuthorsWithNullUser });
+ .spyOn(wrapper.vm.config, 'fetchUsers')
+ .mockResolvedValue({ data: mockUsersWithNullUser });
getBaseToken().vm.$emit('fetch-suggestions', 'root');
});
@@ -126,7 +126,7 @@ describe('AuthorToken', () => {
describe('when res.data is present', () => {
it('filters the successful response when null values are present', () => {
return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
});
@@ -134,14 +134,14 @@ describe('AuthorToken', () => {
describe('when response is an array', () => {
it('filters the successful response when null values are present', () => {
return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
});
});
it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
+ jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-suggestions', 'root');
@@ -153,7 +153,7 @@ describe('AuthorToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
+ jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-suggestions', 'root');
@@ -174,23 +174,23 @@ describe('AuthorToken', () => {
it('renders base-token component', () => {
wrapper = createComponent({
- value: { data: mockAuthors[0].username },
- data: { authors: mockAuthors },
+ value: { data: mockUsers[0].username },
+ data: { users: mockUsers },
});
const baseTokenEl = getBaseToken();
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
- suggestions: mockAuthors,
- getActiveTokenValue: wrapper.vm.getActiveAuthor,
+ suggestions: mockUsers,
+ getActiveTokenValue: wrapper.vm.getActiveUser,
});
});
it('renders token item when value is selected', async () => {
wrapper = createComponent({
- value: { data: mockAuthors[0].username },
- data: { authors: mockAuthors },
+ value: { data: mockUsers[0].username },
+ data: { users: mockUsers },
stubs: { Portal: true },
});
@@ -201,20 +201,20 @@ describe('AuthorToken', () => {
const tokenValue = tokenSegments.at(2);
- expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url);
- expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator"
+ expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockUsers[0].avatar_url);
+ expect(tokenValue.text()).toBe(mockUsers[0].name); // "Administrator"
});
- it('renders token value with correct avatarUrl from author object', async () => {
+ it('renders token value with correct avatarUrl from user object', async () => {
const getAvatarEl = () =>
wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
wrapper = createComponent({
- value: { data: mockAuthors[0].username },
+ value: { data: mockUsers[0].username },
data: {
- authors: [
+ users: [
{
- ...mockAuthors[0],
+ ...mockUsers[0],
},
],
},
@@ -223,15 +223,15 @@ describe('AuthorToken', () => {
await nextTick();
- expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+ expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({
- authors: [
+ users: [
{
- ...mockAuthors[0],
- avatarUrl: mockAuthors[0].avatar_url,
+ ...mockUsers[0],
+ avatarUrl: mockUsers[0].avatar_url,
avatar_url: undefined,
},
],
@@ -239,14 +239,14 @@ describe('AuthorToken', () => {
await nextTick();
- expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+ expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
});
- it('renders provided defaultAuthors as suggestions', async () => {
- const defaultAuthors = DEFAULT_NONE_ANY;
+ it('renders provided defaultUsers as suggestions', async () => {
+ const defaultUsers = OPTIONS_NONE_ANY;
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken, defaultAuthors, preloadedAuthors: mockPreloadedAuthors },
+ config: { ...mockAuthorToken, defaultUsers, preloadedUsers: mockPreloadedUsers },
stubs: { Portal: true },
});
@@ -254,16 +254,16 @@ describe('AuthorToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(defaultAuthors.length + currentUserLength);
- defaultAuthors.forEach((label, index) => {
+ expect(suggestions).toHaveLength(defaultUsers.length + currentUserLength);
+ defaultUsers.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
- it('does not render divider when no defaultAuthors', async () => {
+ it('does not render divider when no defaultUsers', async () => {
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken, defaultAuthors: [] },
+ config: { ...mockAuthorToken, defaultUsers: [] },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
@@ -274,10 +274,10 @@ describe('AuthorToken', () => {
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
- it('renders `DEFAULT_NONE_ANY` as default suggestions', async () => {
+ it('renders `OPTIONS_NONE_ANY` as default suggestions', async () => {
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors },
+ config: { ...mockAuthorToken, preloadedUsers: mockPreloadedUsers },
stubs: { Portal: true },
});
@@ -286,8 +286,8 @@ describe('AuthorToken', () => {
const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(2 + currentUserLength);
- expect(suggestions.at(0).text()).toBe(DEFAULT_NONE_ANY[0].text);
- expect(suggestions.at(1).text()).toBe(DEFAULT_NONE_ANY[1].text);
+ expect(suggestions.at(0).text()).toBe(OPTIONS_NONE_ANY[0].text);
+ expect(suggestions.at(1).text()).toBe(OPTIONS_NONE_ANY[1].text);
});
it('emits listeners in the base-token', () => {
@@ -308,8 +308,8 @@ describe('AuthorToken', () => {
active: true,
config: {
...mockAuthorToken,
- preloadedAuthors: mockPreloadedAuthors,
- defaultAuthors: [],
+ preloadedUsers: mockPreloadedUsers,
+ defaultUsers: [],
},
stubs: { Portal: true },
});
diff --git a/spec/frontend/vue_shared/components/group_select/group_select_spec.js b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
index f959d2225fa..c10b32c6acc 100644
--- a/spec/frontend/vue_shared/components/group_select/group_select_spec.js
+++ b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
@@ -31,7 +31,7 @@ describe('GroupSelect', () => {
const inputId = 'inputId';
// Finders
- const findListbox = () => wrapper.findComponent(GlListbox);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findInput = () => wrapper.findByTestId('input');
// Helpers
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
new file mode 100644
index 00000000000..cb7262b15e3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -0,0 +1,132 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlListbox } from '@gitlab/ui';
+import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
+
+describe('ListboxInput', () => {
+ let wrapper;
+
+ // Props
+ const name = 'name';
+ const defaultToggleText = 'defaultToggleText';
+ const items = [
+ {
+ text: 'Group 1',
+ options: [
+ { text: 'Item 1', value: '1' },
+ { text: 'Item 2', value: '2' },
+ ],
+ },
+ {
+ text: 'Group 2',
+ options: [{ text: 'Item 3', value: '3' }],
+ },
+ ];
+
+ // Finders
+ const findGlListbox = () => wrapper.findComponent(GlListbox);
+ const findInput = () => wrapper.find('input');
+
+ const createComponent = (propsData) => {
+ wrapper = shallowMount(ListboxInput, {
+ propsData: {
+ name,
+ defaultToggleText,
+ items,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('input attributes', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets the input name', () => {
+ expect(findInput().attributes('name')).toBe(name);
+ });
+ });
+
+ describe('toggle text', () => {
+ it('uses the default toggle text while no value is selected', () => {
+ createComponent();
+
+ expect(findGlListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+
+ it("uses the selected option's text as the toggle text", () => {
+ const selectedOption = items[0].options[0];
+ createComponent({ selected: selectedOption.value });
+
+ expect(findGlListbox().props('toggleText')).toBe(selectedOption.text);
+ });
+ });
+
+ describe('input value', () => {
+ const selectedOption = items[0].options[0];
+
+ beforeEach(() => {
+ createComponent({ selected: selectedOption.value });
+ jest.spyOn(findInput().element, 'dispatchEvent');
+ });
+
+ it("sets the listbox's and input's values", () => {
+ const { value } = selectedOption;
+
+ expect(findGlListbox().props('selected')).toBe(value);
+ expect(findInput().attributes('value')).toBe(value);
+ });
+
+ describe("when the listbox's value changes", () => {
+ const newSelectedOption = items[1].options[0];
+
+ beforeEach(() => {
+ findGlListbox().vm.$emit('select', newSelectedOption.value);
+ });
+
+ it('emits the `select` event', () => {
+ expect(wrapper.emitted('select')).toEqual([[newSelectedOption.value]]);
+ });
+ });
+ });
+
+ describe('search', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes all items to GlListbox by default', () => {
+ createComponent();
+ expect(findGlListbox().props('items')).toStrictEqual(items);
+ });
+
+ describe('with groups', () => {
+ beforeEach(() => {
+ createComponent();
+ findGlListbox().vm.$emit('search', '1');
+ });
+
+ it('passes only the items that match the search string', async () => {
+ expect(findGlListbox().props('items')).toStrictEqual([
+ {
+ text: 'Group 1',
+ options: [{ text: 'Item 1', value: '1' }],
+ },
+ ]);
+ });
+ });
+
+ describe('with flat items', () => {
+ beforeEach(() => {
+ createComponent({
+ items: items[0].options,
+ });
+ findGlListbox().vm.$emit('search', '1');
+ });
+
+ it('passes only the items that match the search string', async () => {
+ expect(findGlListbox().props('items')).toStrictEqual([{ text: 'Item 1', value: '1' }]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 50864a4bf25..285ea10c813 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,12 +1,14 @@
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue';
import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
@@ -138,15 +140,13 @@ describe('Markdown field component', () => {
});
it('renders markdown preview and GFM', async () => {
- const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
-
previewLink = getPreviewLink();
previewLink.vm.$emit('click', { target: {} });
await axios.waitFor(markdownPreviewPath);
expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
it('calls video.pause() on comment input when isSubmitting is changed to true', async () => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
index be1d840dd29..176ccfc5a69 100644
--- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -1,10 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('Markdown Field View component', () => {
- let renderGFMSpy;
let wrapper;
function createComponent() {
@@ -12,7 +13,6 @@ describe('Markdown Field View component', () => {
}
beforeEach(() => {
- renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
createComponent();
});
@@ -21,6 +21,6 @@ describe('Markdown Field View component', () => {
});
it('processes rendering with GFM', () => {
- expect(renderGFMSpy).toHaveBeenCalledTimes(1);
+ expect(renderGFM).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 625e67c7cc1..5f416db2676 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -171,6 +171,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect.objectContaining({
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
+ useBottomToolbar: false,
markdown: value,
}),
);
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
index 8edcb905096..2b311b75f85 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -1,5 +1,5 @@
import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MarkdownDrawer, { cache } from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
import { getRenderedMarkdown } from '~/vue_shared/components/markdown_drawer/utils/fetch';
@@ -82,7 +82,10 @@ describe('MarkdownDrawer', () => {
contentTop.mockClear();
});
- it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, () => {
+ it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, async () => {
+ wrapper.vm.getDrawerTop();
+ await Vue.nextTick();
+
expect(findDrawer().attributes('headerheight')).toBe(`${navbarHeight}px`);
});
});
@@ -95,11 +98,11 @@ describe('MarkdownDrawer', () => {
renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
fetchMarkdownSpy = jest.spyOn(MarkdownDrawer.methods, 'fetchMarkdown');
global.document.querySelector = jest.fn(() => ({
- getBoundingClientRect: jest.fn(() => ({ bottom: 100 })),
dataset: {
page: 'test',
},
}));
+ contentTop.mockReturnValue(100);
createComponent();
await nextTick();
});
@@ -118,12 +121,28 @@ describe('MarkdownDrawer', () => {
expect(fetchMarkdownSpy).toHaveBeenCalledTimes(2);
});
- it('for open triggers renderGLFM', async () => {
+ it('triggers renderGLFM in openDrawer', async () => {
wrapper.vm.fetchMarkdown();
wrapper.vm.openDrawer();
await nextTick();
expect(renderGLFMSpy).toHaveBeenCalled();
});
+
+ it('triggers height calculation in openDrawer', async () => {
+ expect(findDrawer().attributes('headerheight')).toBe(`${0}px`);
+ wrapper.vm.fetchMarkdown();
+ wrapper.vm.openDrawer();
+ await nextTick();
+ expect(findDrawer().attributes('headerheight')).toBe(`${100}px`);
+ });
+
+ it('triggers height calculation in toggleDrawer', async () => {
+ expect(findDrawer().attributes('headerheight')).toBe(`${0}px`);
+ wrapper.vm.fetchMarkdown();
+ wrapper.vm.toggleDrawer();
+ await nextTick();
+ expect(findDrawer().attributes('headerheight')).toBe(`${100}px`);
+ });
});
describe('Markdown fetching', () => {
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
index ff07b2cf838..adcf57b76a4 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
@@ -20,9 +20,9 @@ describe('utils/fetch', () => {
});
describe.each`
- axiosMock | type | toExpect
- ${{ code: 200, res: { html: MOCK_HTML } }} | ${'success'} | ${MOCK_DRAWER_DATA}
- ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
+ axiosMock | type | toExpect
+ ${{ code: 200, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
+ ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
`('process markdown data', ({ axiosMock, type, toExpect }) => {
describe(`if api fetch responds with ${type}`, () => {
beforeEach(() => {
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 98b04ede943..559f9bcb1a8 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,10 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
-import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import axios from '~/lib/utils/axios_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
describe('system note component', () => {
let vm;
@@ -75,11 +77,9 @@ describe('system note component', () => {
});
it('should renderGFM onMount', () => {
- const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
-
createComponent(props);
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
it('renders outdated code lines', async () => {
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json
index b42ec42d8b8..e5678c9a956 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/mocks/items_filters.json
@@ -1,14 +1,20 @@
[
- {
- "type": "assignee_username",
- "value": { "data": "root2" }
- },
- {
- "type": "author_username",
- "value": { "data": "root" }
- },
- {
- "type": "filtered-search-term",
- "value": { "data": "bar" }
+ {
+ "type": "assignee",
+ "value": {
+ "data": "root2"
}
- ] \ No newline at end of file
+ },
+ {
+ "type": "author",
+ "value": {
+ "data": "root"
+ }
+ },
+ {
+ "type": "filtered-search-term",
+ "value": {
+ "data": "bar"
+ }
+ }
+] \ No newline at end of file
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index c0c3c4a9729..86a63db0d9e 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -2,9 +2,15 @@ import { GlAlert, GlBadge, GlPagination, GlTabs, GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Tracking from '~/tracking';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATORS_IS,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import PageWrapper from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import mockItems from './mocks/items.json';
import mockFilters from './mocks/items_filters.json';
@@ -166,7 +172,7 @@ describe('AlertManagementEmptyState', () => {
it('renders the filter set with the tokens according to the prop filterSearchTokens', () => {
mountComponent({
- props: { filterSearchTokens: ['assignee_username'] },
+ props: { filterSearchTokens: [TOKEN_TYPE_ASSIGNEE] },
});
expect(Filters().exists()).toBe(true);
@@ -287,26 +293,26 @@ describe('AlertManagementEmptyState', () => {
expect(Filters().props('searchInputPlaceholder')).toBe('Search or filter results…');
expect(Filters().props('tokens')).toEqual([
{
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
icon: 'user',
- title: 'Author',
+ title: TOKEN_TITLE_AUTHOR,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: '/link',
- fetchAuthors: expect.any(Function),
+ fetchUsers: expect.any(Function),
},
{
- type: 'assignee_username',
+ type: TOKEN_TYPE_ASSIGNEE,
icon: 'user',
- title: 'Assignee',
+ title: TOKEN_TITLE_ASSIGNEE,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: '/link',
- fetchAuthors: expect.any(Function),
+ fetchUsers: expect.any(Function),
},
]);
expect(Filters().props('recentSearchesStorageKey')).toBe('items');
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
index fa7fabfaef6..591447a37c2 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -1,6 +1,6 @@
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import component from '~/vue_shared/components/registry/registry_search.vue';
describe('Registry Search', () => {
diff --git a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
index bc1545014d7..79cacadd6af 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
@@ -1,119 +1,5 @@
-export const mockGraphqlRunnerPlatforms = {
- data: {
- runnerPlatforms: {
- nodes: [
- {
- name: 'linux',
- humanReadableName: 'Linux',
- architectures: {
- nodes: [
- {
- name: 'amd64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
- __typename: 'RunnerArchitecture',
- },
- {
- name: '386',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
- __typename: 'RunnerArchitecture',
- },
- {
- name: 'arm',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
- __typename: 'RunnerArchitecture',
- },
- {
- name: 'arm64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
- __typename: 'RunnerArchitecture',
- },
- ],
- __typename: 'RunnerArchitectureConnection',
- },
- __typename: 'RunnerPlatform',
- },
- {
- name: 'osx',
- humanReadableName: 'macOS',
- architectures: {
- nodes: [
- {
- name: 'amd64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
- __typename: 'RunnerArchitecture',
- },
- ],
- __typename: 'RunnerArchitectureConnection',
- },
- __typename: 'RunnerPlatform',
- },
- {
- name: 'windows',
- humanReadableName: 'Windows',
- architectures: {
- nodes: [
- {
- name: 'amd64',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
- __typename: 'RunnerArchitecture',
- },
- {
- name: '386',
- downloadLocation:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
- __typename: 'RunnerArchitecture',
- },
- ],
- __typename: 'RunnerArchitectureConnection',
- },
- __typename: 'RunnerPlatform',
- },
- {
- name: 'docker',
- humanReadableName: 'Docker',
- architectures: null,
- __typename: 'RunnerPlatform',
- },
- {
- name: 'kubernetes',
- humanReadableName: 'Kubernetes',
- architectures: null,
- __typename: 'RunnerPlatform',
- },
- ],
- __typename: 'RunnerPlatformConnection',
- },
- project: { id: 'gid://gitlab/Project/1', __typename: 'Project' },
- group: null,
- },
-};
+import mockGraphqlRunnerPlatforms from 'test_fixtures/graphql/runner_instructions/get_runner_platforms.query.graphql.json';
+import mockGraphqlInstructions from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.json';
+import mockGraphqlInstructionsWindows from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.windows.json';
-export const mockGraphqlInstructions = {
- data: {
- runnerSetup: {
- installInstructions:
- '# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start',
- registerInstructions:
- 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token $REGISTRATION_TOKEN',
- __typename: 'RunnerSetup',
- },
- },
-};
-
-export const mockGraphqlInstructionsWindows = {
- data: {
- runnerSetup: {
- installInstructions:
- '# Windows runner, then run\n.gitlab-runner.exe install\n.gitlab-runner.exe start',
- registerInstructions:
- './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token $REGISTRATION_TOKEN',
- __typename: 'RunnerSetup',
- },
- },
-};
+export { mockGraphqlRunnerPlatforms, mockGraphqlInstructions, mockGraphqlInstructionsWindows };
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 7c5fc63856a..ae9157591c5 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -113,10 +113,7 @@ describe('RunnerInstructionsModal component', () => {
});
describe('should display default instructions', () => {
- const {
- installInstructions,
- registerInstructions,
- } = mockGraphqlInstructions.data.runnerSetup;
+ const { installInstructions } = mockGraphqlInstructions.data.runnerSetup;
it('runner instructions are requested', () => {
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
@@ -128,53 +125,16 @@ describe('RunnerInstructionsModal component', () => {
it('binary instructions are shown', async () => {
const instructions = findBinaryInstructions().text();
- expect(instructions).toBe(installInstructions);
+ expect(instructions).toBe(installInstructions.trim());
});
it('register command is shown with a replaced token', async () => {
const command = findRegisterCommand().text();
expect(command).toBe(
- 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ 'sudo gitlab-runner register --url http://localhost/ --registration-token MY_TOKEN',
);
});
-
- describe('when a register token is not shown', () => {
- beforeEach(async () => {
- createComponent({ props: { registrationToken: undefined } });
- await waitForPromises();
- });
-
- it('register command is shown without a defined registration token', () => {
- const instructions = findRegisterCommand().text();
-
- expect(instructions).toBe(registerInstructions);
- });
- });
-
- describe('when providing a defaultPlatformName', () => {
- beforeEach(async () => {
- createComponent({ props: { defaultPlatformName: 'osx' } });
- await waitForPromises();
- });
-
- it('runner instructions for the default selected platform are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
- platform: 'osx',
- architecture: 'amd64',
- });
- });
-
- it('sets the focus on the default selected platform', () => {
- const findOsxPlatformButton = () => wrapper.findComponent({ ref: 'osx' });
-
- findOsxPlatformButton().element.focus = jest.fn();
-
- findModal().vm.$emit('shown');
-
- expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
- });
- });
});
describe('after a platform and architecture are selected', () => {
@@ -207,14 +167,14 @@ describe('RunnerInstructionsModal component', () => {
it('other binary instructions are shown', () => {
const instructions = findBinaryInstructions().text();
- expect(instructions).toBe(installInstructions);
+ expect(instructions).toBe(installInstructions.trim());
});
it('register command is shown', () => {
const command = findRegisterCommand().text();
expect(command).toBe(
- './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ './gitlab-runner.exe register --url http://localhost/ --registration-token MY_TOKEN',
);
});
@@ -246,6 +206,43 @@ describe('RunnerInstructionsModal component', () => {
});
});
+ describe('when a register token is not known', () => {
+ beforeEach(async () => {
+ createComponent({ props: { registrationToken: undefined } });
+ await waitForPromises();
+ });
+
+ it('register command is shown without a defined registration token', () => {
+ const instructions = findRegisterCommand().text();
+
+ expect(instructions).toBe(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
+ });
+ });
+
+ describe('with a defaultPlatformName', () => {
+ beforeEach(async () => {
+ createComponent({ props: { defaultPlatformName: 'osx' } });
+ await waitForPromises();
+ });
+
+ it('runner instructions for the default selected platform are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
+ platform: 'osx',
+ architecture: 'amd64',
+ });
+ });
+
+ it('sets the focus on the default selected platform', () => {
+ const findOsxPlatformButton = () => wrapper.findComponent({ ref: 'osx' });
+
+ findOsxPlatformButton().element.focus = jest.fn();
+
+ findModal().vm.$emit('shown');
+
+ expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
+ });
+ });
+
describe('when the modal is not shown', () => {
beforeEach(async () => {
createComponent({ shown: false });
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index d720574ce6d..657bd59dac6 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -1,10 +1,16 @@
+import { nextTick } from 'vue';
import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
-import { scrollToElement } from '~/lib/utils/common_utils';
+import LineHighlighter from '~/blob/line_highlighter';
-jest.mock('~/lib/utils/common_utils');
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
const DEFAULT_PROPS = {
chunkIndex: 2,
@@ -104,12 +110,14 @@ describe('Chunk component', () => {
});
it('does not scroll to route hash if last chunk is not loaded', () => {
- expect(scrollToElement).not.toHaveBeenCalled();
+ expect(LineHighlighter).not.toHaveBeenCalled();
});
- it('scrolls to route hash if last chunk is loaded', () => {
+ it('scrolls to route hash if last chunk is loaded', async () => {
createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
- expect(scrollToElement).toHaveBeenCalledWith(hash, { behavior: 'auto' });
+ await nextTick();
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
index a7b55d7332f..4d38e8ef25d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
@@ -4,15 +4,16 @@ import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/g
import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker';
import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker';
import composerJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker';
+import goSumLinker from '~/vue_shared/components/source_viewer/plugins/utils/go_sum_linker';
import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
import {
PACKAGE_JSON_FILE_TYPE,
- PACKAGE_JSON_CONTENT,
GEMSPEC_FILE_TYPE,
GODEPS_JSON_FILE_TYPE,
GEMFILE_FILE_TYPE,
PODSPEC_JSON_FILE_TYPE,
COMPOSER_JSON_FILE_TYPE,
+ GO_SUM_FILE_TYPE,
} from './mock_data';
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
@@ -21,37 +22,31 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linke
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker');
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker');
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/go_sum_linker');
describe('Highlight.js plugin for linking dependencies', () => {
const hljsResultMock = { value: 'test' };
- it('calls packageJsonLinker for package_json file types', () => {
- linkDependencies(hljsResultMock, PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT);
- expect(packageJsonLinker).toHaveBeenCalled();
- });
-
- it('calls gemspecLinker for gemspec file types', () => {
- linkDependencies(hljsResultMock, GEMSPEC_FILE_TYPE);
- expect(gemspecLinker).toHaveBeenCalled();
- });
-
- it('calls godepsJsonLinker for godeps_json file types', () => {
- linkDependencies(hljsResultMock, GODEPS_JSON_FILE_TYPE);
- expect(godepsJsonLinker).toHaveBeenCalled();
- });
-
- it('calls gemfileLinker for gemfile file types', () => {
- linkDependencies(hljsResultMock, GEMFILE_FILE_TYPE);
- expect(gemfileLinker).toHaveBeenCalled();
- });
-
- it('calls podspecJsonLinker for podspec_json file types', () => {
- linkDependencies(hljsResultMock, PODSPEC_JSON_FILE_TYPE);
- expect(podspecJsonLinker).toHaveBeenCalled();
- });
-
- it('calls composerJsonLinker for composer_json file types', () => {
- linkDependencies(hljsResultMock, COMPOSER_JSON_FILE_TYPE);
- expect(composerJsonLinker).toHaveBeenCalled();
+ describe.each`
+ fileType | linker
+ ${PACKAGE_JSON_FILE_TYPE} | ${packageJsonLinker}
+ ${GEMSPEC_FILE_TYPE} | ${gemspecLinker}
+ ${GODEPS_JSON_FILE_TYPE} | ${godepsJsonLinker}
+ ${GEMFILE_FILE_TYPE} | ${gemfileLinker}
+ ${PODSPEC_JSON_FILE_TYPE} | ${podspecJsonLinker}
+ ${COMPOSER_JSON_FILE_TYPE} | ${composerJsonLinker}
+ ${GO_SUM_FILE_TYPE} | ${goSumLinker}
+ `('$fileType file type', ({ fileType, linker }) => {
+ it('calls the correct linker', () => {
+ linkDependencies(hljsResultMock, fileType);
+ expect(linker).toHaveBeenCalled();
+ });
+
+ it('does not call the linker for non-matching file types', () => {
+ const unknownFileType = 'unknown';
+
+ linkDependencies(hljsResultMock, unknownFileType);
+ expect(linker).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
index 5455479ec71..631baf19a2d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
@@ -32,3 +32,5 @@ export const PODSPEC_JSON_CONTENT = `{
}`;
export const COMPOSER_JSON_FILE_TYPE = 'composer_json';
+
+export const GO_SUM_FILE_TYPE = 'go_sum';
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js
new file mode 100644
index 00000000000..cc3ee41523f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js
@@ -0,0 +1,14 @@
+import goSumLinker from '~/vue_shared/components/source_viewer/plugins/utils/go_sum_linker';
+
+describe('Highlight.js plugin for linking go.sum dependencies', () => {
+ it('mutates the input value by wrapping dependencies and tags in anchors', () => {
+ const inputValue =
+ '<span class="">cloud.google.com/Go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</span>';
+ const outputValue =
+ '<span class=""><a href="https://pkg.go.dev/cloud.google.com/go/bigquery" target="_blank" rel="nofollow noreferrer noopener">cloud.google.com/Go/bigquery</a> v1.0.1/go.mod h1:<a href="https://sum.golang.org/lookup/cloud.google.com/go/bigquery@v1.0.1" target="_blank" rel="nofollow noreferrer noopener">i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</a></span>';
+ const hljsResultMock = { value: inputValue };
+
+ const output = goSumLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 4188adc72a1..874796f653a 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -10,7 +10,7 @@ import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_
import { IssuableType } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
-import getIssueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import getIssueParticipantsQuery from '~/sidebar/queries/get_issue_participants.query.graphql';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import {
searchResponse,
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 a0b868d1d52..3b0f0fe6e73 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,13 +1,20 @@
-import { GlModal } from '@gitlab/ui';
+import { GlButton, GlModal, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue';
+import WebIdeLink, {
+ i18n,
+ PREFERRED_EDITOR_RESET_KEY,
+ PREFERRED_EDITOR_KEY,
+ KEY_WEB_IDE,
+} from '~/vue_shared/components/web_ide_link.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
@@ -79,9 +86,18 @@ const ACTION_PIPELINE_EDITOR = {
};
describe('Web IDE link component', () => {
+ useLocalStorageSpy();
+
let wrapper;
- function createComponent(props, mountFn = shallowMountExtended) {
+ function createComponent(
+ props,
+ {
+ mountFn = shallowMountExtended,
+ glFeatures = {},
+ userCalloutDismisserSlotProps = { dismiss: jest.fn() },
+ } = {},
+ ) {
wrapper = mountFn(WebIdeLink, {
propsData: {
editUrl: TEST_EDIT_URL,
@@ -91,6 +107,9 @@ describe('Web IDE link component', () => {
forkPath,
...props,
},
+ provide: {
+ glFeatures,
+ },
stubs: {
GlModal: stubComponent(GlModal, {
template: `
@@ -100,10 +119,19 @@ describe('Web IDE link component', () => {
<slot name="modal-footer"></slot>
</div>`,
}),
+ UserCalloutDismisser: stubComponent(UserCalloutDismisser, {
+ render() {
+ return this.$scopedSlots.default(userCalloutDismisserSlotProps);
+ },
+ }),
},
});
}
+ beforeEach(() => {
+ localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, 'true');
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -112,6 +140,8 @@ describe('Web IDE link component', () => {
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findModal = () => wrapper.findComponent(GlModal);
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
+ const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
+ const findNewWebIdeCalloutPopover = () => wrapper.findComponent(GlPopover);
it.each([
{
@@ -322,9 +352,9 @@ describe('Web IDE link component', () => {
});
it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => {
- createComponent({ ...props, needsToFork: true }, mountExtended);
+ createComponent({ ...props, needsToFork: true }, { mountFn: mountExtended });
- await findActionsButton().trigger('click');
+ await findActionsButton().findComponent(GlButton).trigger('click');
expect(findForkConfirmModal().props()).toEqual({
visible: true,
@@ -377,7 +407,7 @@ describe('Web IDE link component', () => {
gitpodEnabled: false,
gitpodText,
},
- mountExtended,
+ { mountFn: mountExtended },
);
findLocalStorageSync().vm.$emit('input', ACTION_GITPOD.key);
@@ -401,4 +431,178 @@ describe('Web IDE link component', () => {
expect(findModal().exists()).toBe(false);
});
});
+
+ describe('Web IDE callout', () => {
+ describe('vscode_web_ide feature flag is enabled and the edit button is not shown', () => {
+ let dismiss;
+
+ beforeEach(() => {
+ dismiss = jest.fn();
+ createComponent(
+ {
+ showEditButton: false,
+ },
+ {
+ glFeatures: { vscodeWebIde: true },
+ userCalloutDismisserSlotProps: { dismiss },
+ },
+ );
+ });
+ it('does not skip the user_callout_dismisser query', () => {
+ expect(findUserCalloutDismisser().props()).toEqual(
+ expect.objectContaining({
+ skipQuery: false,
+ featureName: 'vscode_web_ide_callout',
+ }),
+ );
+ });
+
+ it('mounts new web ide callout popover', () => {
+ expect(findNewWebIdeCalloutPopover().props()).toEqual(
+ expect.objectContaining({
+ showCloseButton: '',
+ target: 'web-ide-link',
+ triggers: 'manual',
+ boundaryPadding: 80,
+ }),
+ );
+ });
+
+ describe.each`
+ calloutStatus | shouldShowCallout | popoverVisibility | tooltipVisibility
+ ${'show'} | ${true} | ${true} | ${false}
+ ${'hide'} | ${false} | ${false} | ${true}
+ `(
+ 'when should $calloutStatus web ide callout',
+ ({ shouldShowCallout, popoverVisibility, tooltipVisibility }) => {
+ beforeEach(() => {
+ createComponent(
+ {
+ showEditButton: false,
+ },
+ {
+ glFeatures: { vscodeWebIde: true },
+ userCalloutDismisserSlotProps: { shouldShowCallout, dismiss },
+ },
+ );
+ });
+
+ it(`popover visibility = ${popoverVisibility}`, () => {
+ expect(findNewWebIdeCalloutPopover().props().show).toBe(popoverVisibility);
+ });
+
+ it(`action button tooltip visibility = ${tooltipVisibility}`, () => {
+ expect(findActionsButton().props().showActionTooltip).toBe(tooltipVisibility);
+ });
+ },
+ );
+
+ it('dismisses the callout when popover close button is clicked', () => {
+ findNewWebIdeCalloutPopover().vm.$emit('close-button-clicked');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+
+ it('dismisses the callout when action button is clicked', () => {
+ findActionsButton().vm.$emit('actionClicked');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+ });
+
+ describe.each`
+ featureFlag | showEditButton
+ ${false} | ${true}
+ ${true} | ${false}
+ ${false} | ${false}
+ `(
+ 'when vscode_web_ide=$featureFlag and showEditButton = $showEditButton',
+ ({ vscodeWebIde, showEditButton }) => {
+ let dismiss;
+
+ beforeEach(() => {
+ dismiss = jest.fn();
+
+ createComponent(
+ {
+ showEditButton,
+ },
+ { glFeatures: { vscodeWebIde }, userCalloutDismisserSlotProps: { dismiss } },
+ );
+ });
+
+ it('skips the user_callout_dismisser query', () => {
+ expect(findUserCalloutDismisser().props().skipQuery).toBe(true);
+ });
+
+ it('displays actions button tooltip', () => {
+ expect(findActionsButton().props().showActionTooltip).toBe(true);
+ });
+
+ it('mounts new web ide callout popover', () => {
+ expect(findNewWebIdeCalloutPopover().exists()).toBe(false);
+ });
+
+ it('does not dismiss the callout when action button is clicked', () => {
+ findActionsButton().vm.$emit('actionClicked');
+
+ expect(dismiss).not.toHaveBeenCalled();
+ });
+ },
+ );
+ });
+
+ describe('when vscode_web_ide feature flag is enabled', () => {
+ describe('when is not showing edit button', () => {
+ describe(`when ${PREFERRED_EDITOR_RESET_KEY} is unset`, () => {
+ beforeEach(() => {
+ localStorage.setItem.mockReset();
+ localStorage.getItem.mockReturnValueOnce(null);
+ createComponent({ showEditButton: false }, { glFeatures: { vscodeWebIde: true } });
+ });
+
+ it(`sets ${PREFERRED_EDITOR_KEY} local storage key to ${KEY_WEB_IDE}`, () => {
+ expect(localStorage.getItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ expect(localStorage.setItem).toHaveBeenCalledWith(PREFERRED_EDITOR_KEY, KEY_WEB_IDE);
+ });
+
+ it(`sets ${PREFERRED_EDITOR_RESET_KEY} local storage key to true`, () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY, true);
+ });
+
+ it(`selects ${KEY_WEB_IDE} as the preferred editor`, () => {
+ expect(findActionsButton().props().selectedKey).toBe(KEY_WEB_IDE);
+ });
+ });
+
+ describe(`when ${PREFERRED_EDITOR_RESET_KEY} is set to true`, () => {
+ beforeEach(() => {
+ localStorage.setItem.mockReset();
+ localStorage.getItem.mockReturnValueOnce('true');
+ createComponent({ showEditButton: false }, { glFeatures: { vscodeWebIde: true } });
+ });
+
+ it(`does not update the persisted preferred editor`, () => {
+ expect(localStorage.getItem).toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ expect(localStorage.setItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ });
+ });
+ });
+
+ describe('when is showing the edit button', () => {
+ it(`does not try to reset the ${PREFERRED_EDITOR_KEY}`, () => {
+ createComponent({ showEditButton: true }, { glFeatures: { vscodeWebIde: true } });
+
+ expect(localStorage.getItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ });
+ });
+ });
+
+ describe('when vscode_web_ide feature flag is disabled', () => {
+ it(`does not try to reset the ${PREFERRED_EDITOR_KEY}`, () => {
+ createComponent({}, { glFeatures: { vscodeWebIde: false } });
+
+ expect(localStorage.getItem).not.toHaveBeenCalledWith(PREFERRED_EDITOR_RESET_KEY);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index f98e7a678f4..ff21b3bc356 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
const createComponent = ({
descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
new file mode 100644
index 00000000000..76b6efa15b6
--- /dev/null
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
@@ -0,0 +1,141 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import {
+ mockRegularLabel,
+ mockScopedLabel,
+} from 'jest/sidebar/components/labels/labels_select_widget/mock_data';
+import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
+import {
+ DropdownVariant,
+ LabelType,
+} from '~/sidebar/components/labels/labels_select_widget/constants';
+import { WorkspaceType } from '~/issues/constants';
+import { __ } from '~/locale';
+
+const allowLabelRemove = true;
+const attrWorkspacePath = '/workspace-path';
+const fieldName = 'field_name[]';
+const fullPath = '/full-path';
+const labelsFilterBasePath = '/labels-filter-base-path';
+const initialLabels = [];
+const issuableType = 'issue';
+const labelType = LabelType.project;
+const variant = DropdownVariant.Embedded;
+const workspaceType = WorkspaceType.project;
+
+describe('IssuableLabelSelector', () => {
+ let wrapper;
+
+ const findTitle = () => wrapper.find('label').text().replace(/\s+/, ' ');
+ const findLabelIcon = () => wrapper.findComponent(GlIcon);
+ const findAllHiddenInputs = () => wrapper.findAll('input[type="hidden"]');
+ const findLabelSelector = () => wrapper.findComponent(LabelsSelect);
+
+ const createComponent = (injectedProps = {}) => {
+ return shallowMount(IssuableLabelSelector, {
+ provide: {
+ allowLabelRemove,
+ attrWorkspacePath,
+ fieldName,
+ fullPath,
+ labelsFilterBasePath,
+ initialLabels,
+ issuableType,
+ labelType,
+ variant,
+ workspaceType,
+ ...injectedProps,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const expectTitleWithCount = (count) => {
+ const title = findTitle();
+
+ expect(title).toContain(__('Labels'));
+ expect(title).toContain(count.toString());
+ };
+
+ describe('by default', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('has the selected labels count', () => {
+ expectTitleWithCount(0);
+ expect(findLabelIcon().props('name')).toBe('labels');
+ });
+
+ it('has the label selector', () => {
+ expect(findLabelSelector().props()).toMatchObject({
+ allowLabelRemove,
+ allowMultiselect: true,
+ showEmbeddedLabelsList: true,
+ fullPath,
+ attrWorkspacePath,
+ labelsFilterBasePath,
+ dropdownButtonText: __('Select label'),
+ labelsListTitle: __('Select label'),
+ footerCreateLabelTitle: __('Create project label'),
+ footerManageLabelTitle: __('Manage project labels'),
+ variant,
+ workspaceType,
+ labelCreateType: labelType,
+ selectedLabels: initialLabels,
+ });
+
+ expect(findLabelSelector().text()).toBe(__('None'));
+ });
+ });
+
+ it('passing initial labels applies them to the form', () => {
+ wrapper = createComponent({ initialLabels: [mockRegularLabel, mockScopedLabel] });
+
+ expectTitleWithCount(2);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([
+ mockRegularLabel,
+ mockScopedLabel,
+ ]);
+ expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
+ `${mockRegularLabel.id}`,
+ `${mockScopedLabel.id}`,
+ ]);
+ });
+
+ it('updates the selected labels on the `updateSelectedLabels` event', async () => {
+ wrapper = createComponent();
+
+ expectTitleWithCount(0);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([]);
+ expect(findAllHiddenInputs()).toHaveLength(0);
+
+ await findLabelSelector().vm.$emit('updateSelectedLabels', { labels: [mockRegularLabel] });
+
+ expectTitleWithCount(1);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([mockRegularLabel]);
+ expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
+ `${mockRegularLabel.id}`,
+ ]);
+ });
+
+ it('updates the selected labels on the `onLabelRemove` event', async () => {
+ wrapper = createComponent({ initialLabels: [mockRegularLabel] });
+
+ expectTitleWithCount(1);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([mockRegularLabel]);
+ expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
+ `${mockRegularLabel.id}`,
+ ]);
+
+ await findLabelSelector().vm.$emit('onLabelRemove', mockRegularLabel.id);
+
+ expectTitleWithCount(0);
+ expect(findLabelSelector().props('selectedLabels')).toStrictEqual([]);
+ expect(findAllHiddenInputs()).toHaveLength(0);
+ });
+});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index e1c6020686c..2fac004875a 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -225,7 +225,7 @@ describe('IssuableItem', () => {
},
});
- expect(wrapper.findByTestId('issuable-discussions').exists()).toBe(returnValue);
+ expect(wrapper.findByTestId('issuable-comments').exists()).toBe(returnValue);
},
);
});
@@ -489,7 +489,7 @@ describe('IssuableItem', () => {
it('renders discussions count', () => {
wrapper = createComponent();
- const discussionsEl = wrapper.find('[data-testid="issuable-discussions"]');
+ const discussionsEl = wrapper.findByTestId('issuable-comments');
expect(discussionsEl.exists()).toBe(true);
expect(discussionsEl.findComponent(GlLink).attributes()).toMatchObject({
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
index f2211e5b2bb..ea58cc2baf5 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
@@ -1,10 +1,12 @@
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
import IssuableDescription from '~/vue_shared/issuable/show/components/issuable_description.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { mockIssuable } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
const createComponent = ({
issuable = mockIssuable,
enableTaskList = true,
@@ -16,11 +18,9 @@ const createComponent = ({
});
describe('IssuableDescription', () => {
- let renderGFMSpy;
let wrapper;
beforeEach(() => {
- renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
wrapper = createComponent();
});
@@ -30,17 +30,7 @@ describe('IssuableDescription', () => {
describe('mounted', () => {
it('calls `renderGFM`', () => {
- expect(renderGFMSpy).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('methods', () => {
- describe('renderGFM', () => {
- it('calls `renderGFM` on container element', () => {
- wrapper.vm.renderGFM();
-
- expect(renderGFMSpy).toHaveBeenCalled();
- });
+ expect(renderGFM).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
index 3dbff024a6b..aec0f84cb82 100644
--- a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
+++ b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
@@ -141,7 +141,7 @@ exports[`Webhook push events form editor component Different push events rules w
class="form-text text-muted custom-control"
>
<gl-sprintf-stub
- message="Regex such as %{REGEX_CODE} is supported."
+ message="Regular expressions such as %{REGEX_CODE} are supported."
/>
</p>
</gl-form-radio-group-stub>
@@ -367,7 +367,7 @@ exports[`Webhook push events form editor component Different push events rules w
class="form-text text-muted custom-control"
>
<gl-sprintf-stub
- message="Regex such as %{REGEX_CODE} is supported."
+ message="Regular expressions such as %{REGEX_CODE} are supported."
/>
</p>
</gl-form-radio-group-stub>
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js
new file mode 100644
index 00000000000..3e3b8bf65b2
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -0,0 +1,111 @@
+import { GlIcon } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import WorkItemSystemNote from '~/work_items/components/notes/system_note.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import axios from '~/lib/utils/axios_utils';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+describe('system note component', () => {
+ let wrapper;
+ let props;
+ let mock;
+
+ const findTimelineIcon = () => wrapper.findComponent(GlIcon);
+ const findSystemNoteMessage = () => wrapper.findComponent(NoteHeader);
+ const findOutdatedLineButton = () =>
+ wrapper.findComponent('[data-testid="outdated-lines-change-btn"]');
+ const findOutdatedLines = () => wrapper.findComponent('[data-testid="outdated-lines"]');
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(WorkItemSystemNote, {
+ propsData,
+ slots: {
+ 'extra-controls':
+ '<gl-button data-testid="outdated-lines-change-btn">Compare with last version</gl-button>',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ props = {
+ note: {
+ id: '1424',
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatarUrl: 'path',
+ path: '/root',
+ },
+ bodyHtml: '<p dir="auto">closed</p>',
+ systemNoteIconName: 'status_closed',
+ createdAt: '2017-08-02T10:51:58.559Z',
+ },
+ };
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should render a list item with correct id', () => {
+ createComponent(props);
+
+ expect(wrapper.attributes('id')).toBe(`note_${props.note.id}`);
+ });
+
+ // Note: The test case below is to handle a use case related to vuex store but since this does not
+ // have a vuex store , disabling it now will be fixing it in the next iteration
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('should render target class is note is target note', () => {
+ createComponent(props);
+
+ expect(wrapper.classes()).toContain('target');
+ });
+
+ it('should render svg icon', () => {
+ createComponent(props);
+
+ expect(findTimelineIcon().exists()).toBe(true);
+ });
+
+ // Redcarpet Markdown renderer wraps text in `<p>` tags
+ // we need to strip them because they break layout of commit lists in system notes:
+ // https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
+ it('removes wrapping paragraph from note HTML', () => {
+ createComponent(props);
+
+ expect(findSystemNoteMessage().html()).toContain('<span>closed</span>');
+ });
+
+ it('should renderGFM onMount', () => {
+ createComponent(props);
+
+ expect(renderGFM).toHaveBeenCalled();
+ });
+
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('renders outdated code lines', async () => {
+ mock
+ .onGet('/outdated_line_change_path')
+ .reply(200, [
+ { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
+ ]);
+
+ createComponent({
+ note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
+ });
+
+ await findOutdatedLineButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(findOutdatedLines().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 7367212e49f..e85f62b881d 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -435,6 +435,20 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('containerClass')).toBe('gl-shadow-none!');
});
+
+ it('calls the mutation for updating assignees with the correct input', async () => {
+ findTokenSelector().vm.$emit('input', [mockAssignees[1]]);
+ await waitForPromises();
+
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ assigneesWidget: {
+ assigneeIds: [mockAssignees[1].id],
+ },
+ id: 'gid://gitlab/WorkItem/1',
+ },
+ });
+ });
});
describe('tracking', () => {
diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
index 01ab7824975..0ab2546440b 100644
--- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
@@ -1,9 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import $ from 'jquery';
import { nextTick } from 'vue';
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { descriptionTextWithCheckboxes, descriptionHtmlWithCheckboxes } from '../mock_data';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('WorkItemDescription', () => {
let wrapper;
@@ -32,13 +34,11 @@ describe('WorkItemDescription', () => {
});
it('renders gfm', async () => {
- const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
-
createComponent();
await nextTick();
- expect(renderGFMSpy).toHaveBeenCalled();
+ expect(renderGFM).toHaveBeenCalled();
});
describe('with checkboxes', () => {
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index c79b049442d..05476ef5ca0 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -38,7 +38,7 @@ describe('WorkItemDescription', () => {
const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
let workItemResponseHandler;
- let workItemsMvc2;
+ let workItemsMvc;
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
@@ -46,7 +46,7 @@ describe('WorkItemDescription', () => {
const findEditedAt = () => wrapper.findComponent(EditedAt);
const editDescription = (newText) => {
- if (workItemsMvc2) {
+ if (workItemsMvc) {
return findMarkdownEditor().vm.$emit('input', newText);
}
return wrapper.find('textarea').setValue(newText);
@@ -60,6 +60,7 @@ describe('WorkItemDescription', () => {
canUpdate = true,
workItemResponse = workItemResponseFactory({ canUpdate }),
isEditing = false,
+ queryVariables = { id: workItemId },
fetchByIid = false,
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
@@ -75,14 +76,12 @@ describe('WorkItemDescription', () => {
propsData: {
workItemId: id,
fullPath: 'test-project-path',
- queryVariables: {
- id: workItemId,
- },
+ queryVariables,
fetchByIid,
},
provide: {
glFeatures: {
- workItemsMvc2,
+ workItemsMvc,
},
},
stubs: {
@@ -104,11 +103,21 @@ describe('WorkItemDescription', () => {
});
describe.each([true, false])(
- 'editing description with workItemsMvc2 %workItemsMvc2Enabled',
- (workItemsMvc2Enabled) => {
+ 'editing description with workItemsMvc %workItemsMvcEnabled',
+ (workItemsMvcEnabled) => {
beforeEach(() => {
beforeEach(() => {
- workItemsMvc2 = workItemsMvc2Enabled;
+ workItemsMvc = workItemsMvcEnabled;
+ });
+ });
+
+ it('has a subscription', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(subscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
});
});
@@ -275,6 +284,13 @@ describe('WorkItemDescription', () => {
expect(workItemResponseHandler).not.toHaveBeenCalled();
expect(workItemByIidResponseHandler).toHaveBeenCalled();
});
+
+ it('skips calling the handlers when missing the needed queryVariables', async () => {
+ createComponent({ queryVariables: {}, fetchByIid: false });
+ await waitForPromises();
+
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ });
},
);
});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 4029e47c390..686641800b3 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -86,7 +86,7 @@ describe('WorkItemDetailModal component', () => {
isModal: true,
workItemId: defaultPropsData.workItemId,
workItemParentId: defaultPropsData.issueGid,
- iid: null,
+ workItemIid: null,
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 26777b57797..bbab45c7055 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -11,7 +11,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
@@ -21,7 +21,7 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
-import WorkItemInformation from '~/work_items/components/work_item_information.vue';
+import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
@@ -31,7 +31,6 @@ import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assign
import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockParent,
workItemDatesSubscriptionResponse,
@@ -40,11 +39,11 @@ import {
workItemAssigneesSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
projectWorkItemResponse,
+ objectiveType,
} from '../mock_data';
describe('WorkItemDetail component', () => {
let wrapper;
- useLocalStorageSpy();
Vue.use(VueApollo);
@@ -81,8 +80,7 @@ describe('WorkItemDetail component', () => {
const findParentButton = () => findParent().findComponent(GlButton);
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]');
- const findWorkItemInformationAlert = () => wrapper.findComponent(WorkItemInformation);
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
const createComponent = ({
isModal = false,
@@ -92,9 +90,9 @@ describe('WorkItemDetail component', () => {
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
+ workItemsMvcEnabled = false,
workItemsMvc2Enabled = false,
fetchByIid = false,
- iidPathQueryParam = undefined,
} = {}) => {
const handlers = [
[workItemQuery, handler],
@@ -108,7 +106,7 @@ describe('WorkItemDetail component', () => {
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
- propsData: { isModal, workItemId, iid: '1' },
+ propsData: { isModal, workItemId, workItemIid: '1' },
data() {
return {
updateInProgress,
@@ -117,11 +115,14 @@ describe('WorkItemDetail component', () => {
},
provide: {
glFeatures: {
+ workItemsMvc: workItemsMvcEnabled,
workItemsMvc2: workItemsMvc2Enabled,
useIidInWorkItemsPath: fetchByIid,
},
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
+ hasOkrsFeature: true,
+ hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
},
@@ -129,18 +130,12 @@ describe('WorkItemDetail component', () => {
WorkItemWeight: true,
WorkItemIteration: true,
},
- mocks: {
- $route: {
- query: {
- iid_path: iidPathQueryParam,
- },
- },
- },
});
};
afterEach(() => {
wrapper.destroy();
+ setWindowLocation('');
});
describe('when there is no `workItemId` prop', () => {
@@ -406,9 +401,31 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemType().exists()).toBe(false);
});
- it('sets the parent breadcrumb URL', () => {
+ it('shows parent breadcrumb icon', () => {
+ expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
+ });
+
+ it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => {
expect(findParentButton().attributes().href).toBe('../../issues/5');
});
+
+ it('sets the parent breadcrumb URL based on parent webUrl when parent type is not `Issue`', async () => {
+ const mockParentObjective = {
+ parent: {
+ ...mockParent.parent,
+ workItemType: {
+ id: mockParent.parent.workItemType.id,
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ },
+ },
+ };
+ const parentResponse = workItemResponseFactory(mockParentObjective);
+ createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
+ await waitForPromises();
+
+ expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl);
+ });
});
});
@@ -563,7 +580,7 @@ describe('WorkItemDetail component', () => {
`('$description', async ({ milestoneWidgetPresent, exists }) => {
const response = workItemResponseFactory({ milestoneWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler, workItemsMvc2Enabled: true });
+ createComponent({ handler });
await waitForPromises();
expect(findWorkItemMilestone().exists()).toBe(exists);
@@ -594,24 +611,6 @@ describe('WorkItemDetail component', () => {
});
});
- describe('work item information', () => {
- beforeEach(() => {
- createComponent();
- return waitForPromises();
- });
-
- it('is visible when viewed for the first time and sets localStorage value', async () => {
- localStorage.clear();
- expect(findWorkItemInformationAlert().exists()).toBe(true);
- expect(findLocalStorageSync().props('value')).toBe(true);
- });
-
- it('is not visible after reading local storage input', async () => {
- await findLocalStorageSync().vm.$emit('input', false);
- expect(findWorkItemInformationAlert().exists()).toBe(false);
- });
- });
-
it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => {
createComponent();
await waitForPromises();
@@ -633,6 +632,8 @@ describe('WorkItemDetail component', () => {
});
it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => {
+ setWindowLocation(`?iid_path=true`);
+
createComponent({ fetchByIid: true, iidPathQueryParam: 'true' });
await waitForPromises();
@@ -642,4 +643,24 @@ describe('WorkItemDetail component', () => {
iid: '1',
});
});
+
+ describe('hierarchy widget', () => {
+ it('does not render children tree by default', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findHierarchyTree().exists()).toBe(false);
+ });
+
+ it('renders children tree when work item is an Objective', async () => {
+ const objectiveWorkItem = workItemResponseFactory({
+ workItemType: objectiveType,
+ });
+ const handler = jest.fn().mockResolvedValue(objectiveWorkItem);
+ createComponent({ handler });
+ await waitForPromises();
+
+ expect(findHierarchyTree().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js
deleted file mode 100644
index 887c5f615e9..00000000000
--- a/spec/frontend/work_items/components/work_item_information_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { GlAlert, GlLink } from '@gitlab/ui';
-import WorkItemInformation from '~/work_items/components/work_item_information.vue';
-import { helpPagePath } from '~/helpers/help_page_helper';
-
-const createComponent = () => mount(WorkItemInformation);
-
-describe('Work item information alert', () => {
- let wrapper;
- const tasksHelpPath = helpPagePath('user/tasks');
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findHelpLink = () => wrapper.findComponent(GlLink);
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should be visible', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('should emit `work-item-banner-dismissed` event when cross icon is clicked', () => {
- findAlert().vm.$emit('dismiss');
- expect(wrapper.emitted('work-item-banner-dismissed').length).toBe(1);
- });
-
- it('the alert variant should be tip', () => {
- expect(findAlert().props('variant')).toBe('tip');
- });
-
- it('should have the correct text for title', () => {
- expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle);
- });
-
- it('should have the correct link to work item link', () => {
- expect(findHelpLink().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(tasksHelpPath);
- });
-});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 9f7659b3f8d..083bb5bc4a4 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
+import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -49,6 +49,7 @@ describe('WorkItemLabels component', () => {
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
fetchByIid = false,
+ queryVariables = { id: workItemId },
} = {}) => {
const apolloProvider = createMockApollo([
[workItemQuery, workItemQueryHandler],
@@ -63,9 +64,7 @@ describe('WorkItemLabels component', () => {
workItemId,
canUpdate,
fullPath: 'test-project-path',
- queryVariables: {
- id: workItemId,
- },
+ queryVariables,
fetchByIid,
},
attachTo: document.body,
@@ -251,4 +250,11 @@ describe('WorkItemLabels component', () => {
expect(workItemQuerySuccess).not.toHaveBeenCalled();
expect(workItemByIidResponseHandler).toHaveBeenCalled();
});
+
+ it('skips calling the handlers when missing the needed queryVariables', async () => {
+ createComponent({ queryVariables: {}, fetchByIid: false });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).not.toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
new file mode 100644
index 00000000000..5fbd8e7e1a7
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
@@ -0,0 +1,35 @@
+import { GlDropdownSectionHeader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+const createComponent = () => {
+ return extendedWrapper(shallowMount(OkrActionsSplitButton));
+};
+
+describe('RelatedItemsTree', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('OkrActionsSplitButton', () => {
+ describe('template', () => {
+ it('renders objective and key results sections', () => {
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(0).text()).toContain(
+ 'Objective',
+ );
+
+ expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(1).text()).toContain(
+ 'Key result',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
new file mode 100644
index 00000000000..47489d4796b
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
@@ -0,0 +1,67 @@
+import { GlLabel, GlAvatarsInline } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ItemMilestone from '~/issuable/components/issue_milestone.vue';
+import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
+
+import { mockMilestone, mockAssignees, mockLabels } from '../../mock_data';
+
+describe('WorkItemLinkChildMetadata', () => {
+ let wrapper;
+
+ const createComponent = ({
+ allowsScopedLabels = true,
+ milestone = mockMilestone,
+ assignees = mockAssignees,
+ labels = mockLabels,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinkChildMetadata, {
+ propsData: {
+ allowsScopedLabels,
+ milestone,
+ assignees,
+ labels,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders milestone link button', () => {
+ const milestoneLink = wrapper.findComponent(ItemMilestone);
+
+ expect(milestoneLink.exists()).toBe(true);
+ expect(milestoneLink.props('milestone')).toEqual(mockMilestone);
+ });
+
+ it('renders avatars for assignees', () => {
+ const avatars = wrapper.findComponent(GlAvatarsInline);
+
+ expect(avatars.exists()).toBe(true);
+ expect(avatars.props()).toMatchObject({
+ avatars: mockAssignees,
+ collapsed: true,
+ maxVisible: 2,
+ avatarSize: 24,
+ badgeTooltipProp: 'name',
+ badgeSrOnlyText: '',
+ });
+ });
+
+ it('renders labels', () => {
+ const labels = wrapper.findAllComponents(GlLabel);
+ const mockLabel = mockLabels[0];
+
+ expect(labels).toHaveLength(mockLabels.length);
+ expect(labels.at(0).props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index 1d5472a0473..73d498ad055 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -1,33 +1,73 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
+import Vue from 'vue';
+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 { createAlert } from '~/flash';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
+import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
+import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
+import {
+ WIDGET_TYPE_HIERARCHY,
+ TASK_TYPE_NAME,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+} from '~/work_items/constants';
-import { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data';
+import {
+ workItemTask,
+ workItemObjectiveWithChild,
+ workItemObjectiveNoMetadata,
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ mockMilestone,
+ mockAssignees,
+ mockLabels,
+ workItemHierarchyTreeResponse,
+ workItemHierarchyTreeFailureResponse,
+} from '../../mock_data';
+
+jest.mock('~/flash');
describe('WorkItemLinkChild', () => {
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
let wrapper;
+ let getWorkItemTreeQueryHandler;
+
+ Vue.use(VueApollo);
const createComponent = ({
projectPath = 'gitlab-org/gitlab-test',
canUpdate = true,
issuableGid = WORK_ITEM_ID,
childItem = workItemTask,
+ workItemType = TASK_TYPE_NAME,
+ apolloProvider = null,
} = {}) => {
+ getWorkItemTreeQueryHandler = jest.fn().mockResolvedValue(workItemHierarchyTreeResponse);
+
wrapper = shallowMountExtended(WorkItemLinkChild, {
+ apolloProvider:
+ apolloProvider || createMockApollo([[getWorkItemTreeQuery, getWorkItemTreeQueryHandler]]),
propsData: {
projectPath,
canUpdate,
issuableGid,
childItem,
+ workItemType,
},
});
};
+ beforeEach(() => {
+ createAlert.mockClear();
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -66,7 +106,7 @@ describe('WorkItemLinkChild', () => {
beforeEach(() => {
createComponent();
- titleEl = wrapper.findComponent(GlButton);
+ titleEl = wrapper.findByTestId('item-title');
});
it('renders item title', () => {
@@ -76,16 +116,52 @@ describe('WorkItemLinkChild', () => {
it.each`
action | event | emittedEvent
- ${'clicking'} | ${'click'} | ${'click'}
${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
`('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
+ titleEl.vm.$emit(event);
+
+ expect(wrapper.emitted(emittedEvent)).toEqual([[]]);
+ });
+
+ it('emits click event with correct parameters on clicking title', () => {
const eventObj = {
preventDefault: jest.fn(),
};
- titleEl.vm.$emit(event, eventObj);
+ titleEl.vm.$emit('click', eventObj);
- expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]);
+ expect(wrapper.emitted('click')).toEqual([[eventObj]]);
+ });
+ });
+
+ describe('item metadata', () => {
+ const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata);
+
+ beforeEach(() => {
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+ });
+
+ it('renders item metadata component when item has metadata present', () => {
+ const metadataEl = findMetadataComponent();
+ expect(metadataEl.exists()).toBe(true);
+ expect(metadataEl.props()).toMatchObject({
+ allowsScopedLabels: true,
+ milestone: mockMilestone,
+ assignees: mockAssignees,
+ labels: mockLabels,
+ });
+ });
+
+ it('does not render item metadata component when item has no metadata present', () => {
+ createComponent({
+ childItem: workItemObjectiveNoMetadata,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+
+ expect(findMetadataComponent().exists()).toBe(false);
});
});
@@ -116,7 +192,78 @@ describe('WorkItemLinkChild', () => {
it('removeChild event on menu triggers `click-remove-child` event', () => {
itemMenuEl.vm.$emit('removeChild');
- expect(wrapper.emitted('remove')).toEqual([[workItemTask.id]]);
+ expect(wrapper.emitted('removeChild')).toEqual([[workItemTask.id]]);
+ });
+ });
+
+ describe('nested children', () => {
+ const findExpandButton = () => wrapper.findByTestId('expand-child');
+ const findTreeChildren = () => wrapper.findComponent(WorkItemTreeChildren);
+
+ beforeEach(() => {
+ getWorkItemTreeQueryHandler.mockClear();
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+ });
+
+ it('displays expand button when item has children, children are not displayed by default', () => {
+ expect(findExpandButton().exists()).toBe(true);
+ expect(findTreeChildren().exists()).toBe(false);
+ });
+
+ it('fetches and displays children of item when clicking on expand button', async () => {
+ await findExpandButton().vm.$emit('click');
+
+ expect(findExpandButton().props('loading')).toBe(true);
+ await waitForPromises();
+
+ expect(getWorkItemTreeQueryHandler).toHaveBeenCalled();
+ expect(findTreeChildren().exists()).toBe(true);
+
+ const widgetHierarchy = workItemHierarchyTreeResponse.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+ expect(findTreeChildren().props('children')).toEqual(widgetHierarchy.children.nodes);
+ });
+
+ it('does not fetch children if already fetched once while clicking expand button', async () => {
+ findExpandButton().vm.$emit('click'); // Expand for the first time
+ await waitForPromises();
+
+ expect(findTreeChildren().exists()).toBe(true);
+
+ await findExpandButton().vm.$emit('click'); // Collapse
+ findExpandButton().vm.$emit('click'); // Expand again
+ await waitForPromises();
+
+ expect(getWorkItemTreeQueryHandler).toHaveBeenCalledTimes(1); // ensure children were fetched only once.
+ expect(findTreeChildren().exists()).toBe(true);
+ });
+
+ it('calls createAlert when children fetch request fails on clicking expand button', async () => {
+ const getWorkItemTreeQueryFailureHandler = jest
+ .fn()
+ .mockRejectedValue(workItemHierarchyTreeFailureResponse);
+ const apolloProvider = createMockApollo([
+ [getWorkItemTreeQuery, getWorkItemTreeQueryFailureHandler],
+ ]);
+
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ apolloProvider,
+ });
+
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Object),
+ message: 'Something went wrong while fetching children.',
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 071d5fb715a..bbe460a55ba 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -33,7 +33,7 @@ describe('WorkItemLinksForm', () => {
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
hasIterationsFeature = false,
- workItemsMvc2Enabled = false,
+ workItemsMvcEnabled = false,
parentIteration = null,
formType = FORM_TYPES.create,
} = {}) => {
@@ -52,7 +52,7 @@ describe('WorkItemLinksForm', () => {
},
provide: {
glFeatures: {
- workItemsMvc2: workItemsMvc2Enabled,
+ workItemsMvc: workItemsMvcEnabled,
},
projectPath: 'project/path',
hasIterationsFeature,
@@ -165,23 +165,8 @@ describe('WorkItemLinksForm', () => {
});
describe('associate iteration with task', () => {
- it('does not update iteration when mvc2 feature flag is not enabled', async () => {
- await createComponent({
- hasIterationsFeature: true,
- parentIteration: mockParentIteration,
- });
-
- findInput().vm.$emit('input', 'Create task test');
-
- findForm().vm.$emit('submit', {
- preventDefault: jest.fn(),
- });
- await waitForPromises();
- expect(updateMutationResolver).not.toHaveBeenCalled();
- });
it('updates when parent has an iteration associated', async () => {
await createComponent({
- workItemsMvc2Enabled: true,
hasIterationsFeature: true,
parentIteration: mockParentIteration,
});
@@ -191,18 +176,23 @@ describe('WorkItemLinksForm', () => {
preventDefault: jest.fn(),
});
await waitForPromises();
- expect(updateMutationResolver).toHaveBeenCalledWith({
+ expect(createMutationResolver).toHaveBeenCalledWith({
input: {
- id: 'gid://gitlab/WorkItem/1',
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
iterationWidget: {
iterationId: mockParentIteration.id,
},
},
});
});
- it('does not update when parent has no iteration associated', async () => {
+ it('does not send the iteration widget to mutation when parent has no iteration associated', async () => {
await createComponent({
- workItemsMvc2Enabled: true,
hasIterationsFeature: true,
});
findInput().vm.$emit('input', 'Create task test');
@@ -211,7 +201,20 @@ describe('WorkItemLinksForm', () => {
preventDefault: jest.fn(),
});
await waitForPromises();
- expect(updateMutationResolver).not.toHaveBeenCalled();
+ expect(createMutationResolver).not.toHaveBeenCalledWith({
+ input: {
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
+ iterationWidget: {
+ iterationId: mockParentIteration.id,
+ },
+ },
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 66ce2c1becf..a61de78c623 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -4,20 +4,25 @@ 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 setWindowLocation from 'helpers/set_window_location_helper';
+import { stubComponent } from 'helpers/stub_component';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { FORM_TYPES } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
changeWorkItemParentMutationResponse,
workItemQueryResponse,
+ projectWorkItemResponse,
} from '../../mock_data';
Vue.use(VueApollo);
@@ -55,6 +60,7 @@ const issueDetailsResponse = (confidential = false) => ({
},
},
});
+const showModal = jest.fn();
describe('WorkItemLinks', () => {
let wrapper;
@@ -71,6 +77,7 @@ describe('WorkItemLinks', () => {
.mockResolvedValue(changeWorkItemParentMutationResponse);
const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const childWorkItemByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const createComponent = async ({
data = {},
@@ -78,6 +85,7 @@ describe('WorkItemLinks', () => {
mutationHandler = mutationChangeParentHandler,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
hasIterationsFeature = false,
+ fetchByIid = false,
} = {}) => {
mockApollo = createMockApollo(
[
@@ -85,6 +93,7 @@ describe('WorkItemLinks', () => {
[changeWorkItemParentMutation, mutationHandler],
[workItemQuery, childWorkItemQueryHandler],
[issueDetailsQuery, issueDetailsQueryHandler],
+ [workItemByIidQuery, childWorkItemByIidHandler],
],
{},
{ addTypename: true },
@@ -100,12 +109,22 @@ describe('WorkItemLinks', () => {
projectPath: 'project/path',
iid: '1',
hasIterationsFeature,
+ glFeatures: {
+ useIidInWorkItemsPath: fetchByIid,
+ },
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
mocks: {
$toast,
},
+ stubs: {
+ WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
+ methods: {
+ show: showModal,
+ },
+ }),
+ },
});
await waitForPromises();
@@ -130,6 +149,7 @@ describe('WorkItemLinks', () => {
afterEach(() => {
wrapper.destroy();
mockApollo = null;
+ setWindowLocation('');
});
it('is expanded by default', () => {
@@ -237,7 +257,7 @@ describe('WorkItemLinks', () => {
});
it('calls correct mutation with correct variables', async () => {
- firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
+ firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
await waitForPromises();
@@ -252,7 +272,7 @@ describe('WorkItemLinks', () => {
});
it('shows toast when mutation succeeds', async () => {
- firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
+ firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
await waitForPromises();
@@ -264,56 +284,164 @@ describe('WorkItemLinks', () => {
it('renders correct number of children after removal', async () => {
expect(findWorkItemLinkChildItems()).toHaveLength(4);
- firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
+ firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
await waitForPromises();
expect(findWorkItemLinkChildItems()).toHaveLength(3);
});
});
- describe('prefetching child items', () => {
- let firstChild;
-
- beforeEach(async () => {
- await createComponent();
+ describe('when parent item is confidential', () => {
+ it('passes correct confidentiality status to form', async () => {
+ await createComponent({
+ issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ });
+ findToggleFormDropdown().vm.$emit('click');
+ findToggleAddFormButton().vm.$emit('click');
+ await nextTick();
- firstChild = findFirstWorkItemLinkChild();
+ expect(findAddLinksForm().props('parentConfidential')).toBe(true);
});
+ });
- it('does not fetch the child work item before hovering work item links', () => {
- expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ describe('when work item is fetched by id', () => {
+ describe('prefetching child items', () => {
+ let firstChild;
+
+ beforeEach(async () => {
+ await createComponent();
+
+ firstChild = findFirstWorkItemLinkChild();
+ });
+
+ it('does not fetch the child work item by id before hovering work item links', () => {
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('fetches the child work item by id if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
+ });
+ });
+
+ it('does not fetch the child work item by id if link is hovered for less than 250 ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(200);
+ firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('does not fetch work item by iid if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
+ });
});
- it('fetches the child work item if link is hovered for 250+ ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- await waitForPromises();
+ it('starts prefetching work item by id if URL contains work item id', async () => {
+ setWindowLocation('?work_item_id=5');
+ await createComponent();
expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
+ id: 'gid://gitlab/WorkItem/5',
});
});
- it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(200);
- firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
- await waitForPromises();
+ it('does not open the modal if work item id URL parameter is not found in child items', async () => {
+ setWindowLocation('?work_item_id=555');
+ await createComponent();
+
+ expect(showModal).not.toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(null);
+ });
+
+ it('opens the modal if work item id URL parameter is found in child items', async () => {
+ setWindowLocation('?work_item_id=2');
+ await createComponent();
- expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ expect(showModal).toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(
+ 'gid://gitlab/WorkItem/2',
+ );
});
});
- describe('when parent item is confidential', () => {
- it('passes correct confidentiality status to form', async () => {
- await createComponent({
- issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ describe('when work item is fetched by iid', () => {
+ describe('prefetching child items', () => {
+ let firstChild;
+
+ beforeEach(async () => {
+ setWindowLocation('?iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ firstChild = findFirstWorkItemLinkChild();
});
- findToggleFormDropdown().vm.$emit('click');
- findToggleAddFormButton().vm.$emit('click');
- await nextTick();
- expect(findAddLinksForm().props('parentConfidential')).toBe(true);
+ it('does not fetch the child work item by iid before hovering work item links', () => {
+ expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('fetches the child work item by iid if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
+ fullPath: 'project/path',
+ iid: '2',
+ });
+ });
+
+ it('does not fetch the child work item by iid if link is hovered for less than 250 ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(200);
+ firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
+ await waitForPromises();
+
+ expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('does not fetch work item by id if link is hovered for 250+ ms', async () => {
+ firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ await waitForPromises();
+
+ expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
+ });
});
+
+ it('starts prefetching work item by iid if URL contains work item id', async () => {
+ setWindowLocation('?work_item_iid=5&iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
+ iid: '5',
+ fullPath: 'project/path',
+ });
+ });
+ });
+
+ it('does not open the modal if work item iid URL parameter is not found in child items', async () => {
+ setWindowLocation('?work_item_iid=555&iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ expect(showModal).not.toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null);
+ });
+
+ it('opens the modal if work item iid URL parameter is found in child items', async () => {
+ setWindowLocation('?work_item_iid=2&iid_path=true');
+ await createComponent({ fetchByIid: true });
+
+ expect(showModal).toHaveBeenCalled();
+ expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2');
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
new file mode 100644
index 00000000000..96211e12755
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -0,0 +1,147 @@
+import Vue, { nextTick } from 'vue';
+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 WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
+import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+import {
+ FORM_TYPES,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+} from '~/work_items/constants';
+import { childrenWorkItems, workItemObjectiveWithChild } from '../../mock_data';
+
+describe('WorkItemTree', () => {
+ let getWorkItemQueryHandler;
+ let wrapper;
+
+ const findToggleButton = () => wrapper.findByTestId('toggle-tree');
+ const findTreeBody = () => wrapper.findByTestId('tree-body');
+ const findEmptyState = () => wrapper.findByTestId('tree-empty');
+ const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
+ const findForm = () => wrapper.findComponent(WorkItemLinksForm);
+ const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
+
+ Vue.use(VueApollo);
+
+ const createComponent = ({
+ workItemType = 'Objective',
+ children = childrenWorkItems,
+ apolloProvider = null,
+ } = {}) => {
+ const mockWorkItemResponse = {
+ data: {
+ workItem: {
+ ...workItemObjectiveWithChild,
+ workItemType: {
+ ...workItemObjectiveWithChild.workItemType,
+ name: workItemType,
+ },
+ },
+ },
+ };
+ getWorkItemQueryHandler = jest.fn().mockResolvedValue(mockWorkItemResponse);
+
+ wrapper = shallowMountExtended(WorkItemTree, {
+ apolloProvider:
+ apolloProvider || createMockApollo([[workItemQuery, getWorkItemQueryHandler]]),
+ propsData: {
+ workItemType,
+ workItemId: 'gid://gitlab/WorkItem/515',
+ children,
+ projectPath: 'test/project',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('is expanded by default and displays Add button', () => {
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findTreeBody().exists()).toBe(true);
+ expect(findToggleFormSplitButton().exists()).toBe(true);
+ });
+
+ it('collapses on click toggle button', async () => {
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findTreeBody().exists()).toBe(false);
+ });
+
+ it('displays empty state if there are no children', () => {
+ createComponent({ children: [] });
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('renders all hierarchy widget children', () => {
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
+ });
+
+ it('does not display form by default', () => {
+ expect(findForm().exists()).toBe(false);
+ });
+
+ it.each`
+ option | event | formType | childType
+ ${'New objective'} | ${'showCreateObjectiveForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
+ ${'Existing objective'} | ${'showAddObjectiveForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_OBJECTIVE}
+ ${'New key result'} | ${'showCreateKeyResultForm'} | ${FORM_TYPES.create} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
+ ${'Existing key result'} | ${'showAddKeyResultForm'} | ${FORM_TYPES.add} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT}
+ `(
+ 'when selecting $option from split button, renders the form passing $formType and $childType',
+ async ({ event, formType, childType }) => {
+ findToggleFormSplitButton().vm.$emit(event);
+ await nextTick();
+
+ expect(findForm().exists()).toBe(true);
+ expect(findForm().props('formType')).toBe(formType);
+ expect(findForm().props('childrenType')).toBe(childType);
+ },
+ );
+
+ it('remove event on child triggers `removeChild` event', () => {
+ const firstChild = findWorkItemLinkChildItems().at(0);
+ firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
+
+ expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
+ });
+
+ it.each`
+ description | workItemType | prefetch
+ ${'prefetches'} | ${'Issue'} | ${true}
+ ${'does not prefetch'} | ${'Objective'} | ${false}
+ `(
+ '$description work-item-link-child on mouseover when workItemType is "$workItemType"',
+ async ({ workItemType, prefetch }) => {
+ createComponent({ workItemType });
+ const firstChild = findWorkItemLinkChildItems().at(0);
+ firstChild.vm.$emit('mouseover', childrenWorkItems[0]);
+ await nextTick();
+ await waitForPromises();
+
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ if (prefetch) {
+ expect(getWorkItemQueryHandler).toHaveBeenCalled();
+ } else {
+ expect(getWorkItemQueryHandler).not.toHaveBeenCalled();
+ }
+ },
+ );
+});
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index 60ba2b55f76..5997de01274 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -179,6 +179,18 @@ describe('WorkItemMilestone component', () => {
createComponent({ canUpdate: true });
});
+ it('calls successSearchQueryHandler with variables when dropdown is opened', async () => {
+ showDropdown();
+ await nextTick();
+
+ expect(successSearchQueryHandler).toHaveBeenCalledWith({
+ first: 20,
+ fullPath: 'full-path',
+ state: 'active',
+ title: '',
+ });
+ });
+
it('shows the skeleton loader when the items are being fetched on click', async () => {
showDropdown();
await nextTick();
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
new file mode 100644
index 00000000000..ed68d214fc9
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -0,0 +1,107 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import SystemNote from '~/work_items/components/notes/system_note.vue';
+import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
+import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql';
+import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+import {
+ mockWorkItemNotesResponse,
+ workItemQueryResponse,
+ mockWorkItemNotesByIidResponse,
+} from '../mock_data';
+
+const mockWorkItemId = workItemQueryResponse.data.workItem.id;
+const mockNotesWidgetResponse = mockWorkItemNotesResponse.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+const mockNotesByIidWidgetResponse = mockWorkItemNotesByIidResponse.data.workspace.workItems.nodes[0].widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+describe('WorkItemNotes component', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
+ const findActivityLabel = () => wrapper.find('label');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
+ const workItemNotesByIidQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesByIidResponse);
+
+ const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false } = {}) => {
+ wrapper = shallowMount(WorkItemNotes, {
+ apolloProvider: createMockApollo([
+ [workItemNotesQuery, workItemNotesQueryHandler],
+ [workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
+ ]),
+ propsData: {
+ workItemId,
+ queryVariables: {
+ id: workItemId,
+ },
+ fullPath: 'test-path',
+ fetchByIid,
+ },
+ provide: {
+ glFeatures: {
+ useIidInWorkItemsPath: fetchByIid,
+ },
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders activity label', () => {
+ expect(findActivityLabel().exists()).toBe(true);
+ });
+
+ describe('when notes are loading', () => {
+ it('renders skeleton loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('does not render system notes', () => {
+ expect(findAllSystemNotes().exists()).toBe(false);
+ });
+ });
+
+ describe('when notes have been loaded', () => {
+ it('does not render skeleton loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('renders system notes to the length of the response', async () => {
+ await waitForPromises();
+ expect(findAllSystemNotes()).toHaveLength(mockNotesWidgetResponse.discussions.nodes.length);
+ });
+ });
+
+ describe('when the notes are fetched by `iid`', () => {
+ beforeEach(async () => {
+ createComponent({ workItemId: mockWorkItemId, fetchByIid: true });
+ await waitForPromises();
+ });
+
+ it('shows the notes list', () => {
+ expect(findAllSystemNotes()).toHaveLength(
+ mockNotesByIidWidgetResponse.discussions.nodes.length,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 635a1f326f8..850672b68d0 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -36,6 +36,16 @@ export const mockLabels = [
},
];
+export const mockMilestone = {
+ __typename: 'Milestone',
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ state: 'active',
+ expired: false,
+ startDate: '2022-10-17',
+ dueDate: '2022-10-24',
+};
+
export const workItemQueryResponse = {
data: {
workItem: {
@@ -85,11 +95,18 @@ export const workItemQueryResponse = {
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
+ hasChildren: true,
parent: {
id: 'gid://gitlab/Issue/1',
iid: '5',
title: 'Parent title',
confidential: false,
+ webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
},
children: {
nodes: [
@@ -97,6 +114,20 @@ export const workItemQueryResponse = {
id: 'gid://gitlab/WorkItem/444',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ workItemType: {
+ id: '1',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: false,
+ },
+ ],
},
],
},
@@ -138,13 +169,25 @@ export const updateWorkItemMutationResponse = {
},
widgets: [
{
+ type: 'HIERARCHY',
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ workItemType: {
+ id: '1',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
},
],
},
+ __typename: 'WorkItemConnection',
},
{
__typename: 'WorkItemWidgetAssignees',
@@ -177,6 +220,12 @@ export const mockParent = {
iid: '5',
title: 'Parent title',
confidential: false,
+ webUrl: 'http://gdk.test/gitlab-org/gitlab/-/issues/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
},
};
@@ -193,6 +242,20 @@ export const descriptionHtmlWithCheckboxes = `
</ul>
`;
+const taskType = {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+};
+
+export const objectiveType = {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+};
+
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
@@ -201,8 +264,10 @@ export const workItemResponseFactory = ({
datesWidgetPresent = true,
labelsWidgetPresent = true,
weightWidgetPresent = true,
+ progressWidgetPresent = true,
milestoneWidgetPresent = true,
iterationWidgetPresent = true,
+ healthStatusWidgetPresent = true,
confidential = false,
canInviteMembers = false,
allowsScopedLabels = false,
@@ -210,6 +275,7 @@ export const workItemResponseFactory = ({
lastEditedBy = null,
withCheckboxes = false,
parent = mockParent.parent,
+ workItemType = taskType,
} = {}) => ({
data: {
workItem: {
@@ -227,12 +293,7 @@ export const workItemResponseFactory = ({
id: '1',
fullPath: 'test-project-path',
},
- workItemType: {
- __typename: 'WorkItemType',
- id: 'gid://gitlab/WorkItems::Type/5',
- name: 'Task',
- iconName: 'issue-type-task',
- },
+ workItemType,
userPermissions: {
deleteWorkItem: canDelete,
updateWorkItem: canUpdate,
@@ -298,26 +359,51 @@ export const workItemResponseFactory = ({
},
}
: { type: 'MOCK TYPE' },
+ progressWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetProgress',
+ type: 'PROGRESS',
+ progress: 0,
+ }
+ : { type: 'MOCK TYPE' },
milestoneWidgetPresent
? {
__typename: 'WorkItemWidgetMilestone',
type: 'MILESTONE',
- milestone: {
- expired: false,
- id: 'gid://gitlab/Milestone/30',
- title: 'v4.0',
- },
+ milestone: mockMilestone,
+ }
+ : { type: 'MOCK TYPE' },
+ healthStatusWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetHealthStatus',
+ type: 'HEALTH_STATUS',
+ healthStatus: 'onTrack',
}
: { type: 'MOCK TYPE' },
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
+ hasChildren: true,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ workItemType: {
+ id: '1',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: false,
+ },
+ ],
},
],
},
@@ -637,6 +723,8 @@ export const workItemHierarchyEmptyResponse = {
id: 'gid://gitlab/WorkItem/1',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
@@ -660,6 +748,7 @@ export const workItemHierarchyEmptyResponse = {
{
type: 'HIERARCHY',
parent: null,
+ hasChildren: false,
children: {
nodes: [],
__typename: 'WorkItemConnection',
@@ -678,6 +767,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
id: 'gid://gitlab/WorkItem/1',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
@@ -699,12 +790,16 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
{
type: 'HIERARCHY',
parent: null,
+ hasChildren: true,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'xyz',
@@ -712,6 +807,12 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: false,
+ },
+ ],
__typename: 'WorkItem',
},
],
@@ -727,8 +828,11 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
export const workItemTask = {
id: 'gid://gitlab/WorkItem/4',
+ iid: '4',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'bar',
@@ -741,8 +845,11 @@ export const workItemTask = {
export const confidentialWorkItemTask = {
id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'xyz',
@@ -755,8 +862,11 @@ export const confidentialWorkItemTask = {
export const closedWorkItemTask = {
id: 'gid://gitlab/WorkItem/3',
+ iid: '3',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
__typename: 'WorkItemType',
},
title: 'abc',
@@ -767,12 +877,153 @@ export const closedWorkItemTask = {
__typename: 'WorkItem',
};
+export const childrenWorkItems = [
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ workItemTask,
+ {
+ id: 'gid://gitlab/WorkItem/5',
+ iid: '5',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ __typename: 'WorkItemType',
+ },
+ title: 'foobar',
+ state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ __typename: 'WorkItem',
+ },
+];
+
export const workItemHierarchyResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ },
+ confidential: false,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
+ widgets: [
+ {
+ type: 'DESCRIPTION',
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ hasChildren: true,
+ children: {
+ nodes: childrenWorkItems,
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+
+export const workItemObjectiveWithChild = {
+ id: 'gid://gitlab/WorkItem/12',
+ iid: '12',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ },
+ title: 'Objective',
+ description: 'Objective description',
+ state: 'OPEN',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: true,
+ parent: null,
+ children: {
+ nodes: [],
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ type: 'MILESTONE',
+ __typename: 'WorkItemWidgetMilestone',
+ milestone: mockMilestone,
+ },
+ {
+ type: 'ASSIGNEES',
+ __typename: 'WorkItemWidgetAssignees',
+ canInviteMembers: true,
+ allowsMultipleAssignees: true,
+ assignees: {
+ __typename: 'UserCoreConnection',
+ nodes: mockAssignees,
+ },
+ },
+ {
+ type: 'LABELS',
+ __typename: 'WorkItemWidgetLabels',
+ allowsScopedLabels: true,
+ labels: {
+ __typename: 'LabelConnection',
+ nodes: mockLabels,
+ },
+ },
+ ],
+ __typename: 'WorkItem',
+};
+
+export const workItemObjectiveNoMetadata = {
+ ...workItemObjectiveWithChild,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: true,
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+};
+
+export const workItemHierarchyTreeResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
title: 'New title',
@@ -794,22 +1045,30 @@ export const workItemHierarchyResponse = {
{
type: 'HIERARCHY',
parent: null,
+ hasChildren: true,
children: {
nodes: [
- confidentialWorkItemTask,
- closedWorkItemTask,
- workItemTask,
{
- id: 'gid://gitlab/WorkItem/5',
+ id: 'gid://gitlab/WorkItem/13',
+ iid: '13',
workItemType: {
- id: 'gid://gitlab/WorkItems::Type/5',
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
- title: 'foobar',
+ title: 'Objective 2',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ hasChildren: true,
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
__typename: 'WorkItem',
},
],
@@ -823,6 +1082,15 @@ export const workItemHierarchyResponse = {
},
};
+export const workItemHierarchyTreeFailureResponse = {
+ data: {},
+ errors: [
+ {
+ message: 'Something went wrong',
+ },
+ ],
+};
+
export const changeWorkItemParentMutationResponse = {
data: {
workItemUpdate: {
@@ -856,6 +1124,7 @@ export const changeWorkItemParentMutationResponse = {
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
parent: null,
+ hasChildren: false,
children: {
nodes: [],
},
@@ -1196,3 +1465,288 @@ export const projectWorkItemResponse = {
},
},
};
+
+export const mockWorkItemNotesResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '60',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
+ },
+ {
+ __typename: 'WorkItemWidgetWeight',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/2428',
+ body: 'added #31 as parent issue',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
+ systemNoteIconName: 'link',
+ createdAt: '2022-11-14T04:18:59Z',
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/MilestoneNote/not-persisted',
+ body: 'changed milestone to %5',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
+ systemNoteIconName: 'clock',
+ createdAt: '2022-11-14T04:18:59Z',
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/not-persisted',
+ body: 'changed weight to 89',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'WorkItemWidgetNotes',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+export const mockWorkItemNotesByIidResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/6',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '51',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
+ },
+ {
+ __typename: 'WorkItemWidgetWeight',
+ },
+ {
+ __typename: 'WorkItemWidgetHealthStatus',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0xNCAwNDoxOTowMC4wOTkxMTcwMDAgKzAwMDAiLCJpZCI6IjQyNyIsIl9rZCI6Im4ifQ==',
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/2428',
+ body: 'added #31 as parent issue',
+ bodyHtml:
+ '\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eadded \u003ca href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container="body" data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue"\u003e#31\u003c/a\u003e as parent issue\u003c/p\u003e',
+ systemNoteIconName: 'link',
+ createdAt: '2022-11-14T04:18:59Z',
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ notes: {
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ body: 'changed milestone to %5',
+ bodyHtml:
+ '\u003cp data-sourcepos="1:1-1:23" dir="auto"\u003echanged milestone to \u003ca href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip"\u003e%v4.0\u003c/a\u003e\u003c/p\u003e',
+ systemNoteIconName: 'clock',
+ createdAt: '2022-11-14T04:18:59Z',
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/addbc177f7664699a135130ab05ffb78c57e4db3',
+ notes: {
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
+ body: 'changed iteration to *iteration:5352',
+ bodyHtml:
+ '\u003cp data-sourcepos="1:1-1:36" dir="auto"\u003echanged iteration to \u003ca href="/groups/flightjs/-/iterations/5352" data-reference-type="iteration" data-original="*iteration:5352" data-link="false" data-link-reference="false" data-project="6" data-iteration="5352" data-container="body" data-placement="top" title="Iteration" class="gfm gfm-iteration has-tooltip"\u003eEt autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022\u003c/a\u003e\u003c/p\u003e',
+ systemNoteIconName: 'iteration',
+ createdAt: '2022-11-14T04:19:00Z',
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'WorkItemWidgetNotes',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ ],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 880c4271024..a766962771a 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -55,7 +55,7 @@ describe('Work items root component', () => {
isModal: false,
workItemId: 'gid://gitlab/WorkItem/1',
workItemParentId: null,
- iid: '1',
+ workItemIid: '1',
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 982f9f71f9e..b503d819435 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -1,14 +1,12 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
workItemAssigneesSubscriptionResponse,
workItemDatesSubscriptionResponse,
workItemResponseFactory,
workItemTitleSubscriptionResponse,
- workItemWeightSubscriptionResponse,
workItemLabelsSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
workItemDescriptionSubscriptionResponse,
@@ -25,6 +23,8 @@ import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
+jest.mock('~/behaviors/markdown/render_gfm');
+
describe('Work items router', () => {
let wrapper;
@@ -33,7 +33,6 @@ describe('Work items router', () => {
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
- const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
const assigneesSubscriptionHandler = jest
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
@@ -61,10 +60,6 @@ describe('Work items router', () => {
[workItemDescriptionSubscription, descriptionSubscriptionHandler],
];
- if (IS_EE) {
- handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
- }
-
wrapper = mount(App, {
apolloProvider: createMockApollo(handlers),
router,
@@ -72,6 +67,13 @@ describe('Work items router', () => {
fullPath: 'full-path',
issuesListPath: 'full-path/-/issues',
hasIssueWeightsFeature: false,
+ hasIterationsFeature: false,
+ hasOkrsFeature: false,
+ hasIssuableHealthStatusFeature: false,
+ },
+ stubs: {
+ WorkItemWeight: true,
+ WorkItemIteration: true,
},
});
};