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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/raw_transformer.js6
-rw-r--r--spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap1
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js1
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js18
-rw-r--r--spec/frontend/admin/signup_restrictions/mock_data.js6
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js57
-rw-r--r--spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js107
-rw-r--r--spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap3
-rw-r--r--spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap34
-rw-r--r--spec/frontend/admin/users/components/associations/associations_list_item_spec.js25
-rw-r--r--spec/frontend/admin/users/components/associations/associations_list_spec.js78
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js22
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js7
-rw-r--r--spec/frontend/admin/users/mock_data.js14
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js2
-rw-r--r--spec/frontend/api/groups_api_spec.js27
-rw-r--r--spec/frontend/api/user_api_spec.js17
-rw-r--r--spec/frontend/artifacts/components/artifact_row_spec.js67
-rw-r--r--spec/frontend/artifacts/components/artifacts_table_row_details_spec.js122
-rw-r--r--spec/frontend/artifacts/components/job_artifacts_table_spec.js341
-rw-r--r--spec/frontend/artifacts/graphql/cache_update_spec.js67
-rw-r--r--spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js145
-rw-r--r--spec/frontend/blob/blob_blame_link_spec.js12
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap18
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js105
-rw-r--r--spec/frontend/blob/utils_spec.js62
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js48
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js241
-rw-r--r--spec/frontend/boards/board_list_helper.js2
-rw-r--r--spec/frontend/boards/board_list_spec.js6
-rw-r--r--spec/frontend/boards/components/board_add_new_column_spec.js2
-rw-r--r--spec/frontend/boards/components/board_app_spec.js1
-rw-r--r--spec/frontend/boards/components/board_card_move_to_position_spec.js1
-rw-r--r--spec/frontend/boards/components/board_card_spec.js2
-rw-r--r--spec/frontend/boards/components/board_content_spec.js84
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js44
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js2
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js1
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js11
-rw-r--r--spec/frontend/boards/mock_data.js81
-rw-r--r--spec/frontend/boards/stores/actions_spec.js18
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap139
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js143
-rw-r--r--spec/frontend/branches/mock_data.js7
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js38
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js (renamed from spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js280
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js)19
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js (renamed from spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js40
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js (renamed from spec/frontend/pipeline_schedules/mock_data.js)27
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js (renamed from spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js)26
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js (renamed from spec/frontend/runner/admin_runners/admin_runners_app_spec.js)39
-rw-r--r--spec/frontend/ci/runner/components/__snapshots__/runner_status_popover_spec.js.snap (renamed from spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap)0
-rw-r--r--spec/frontend/ci/runner/components/cells/link_cell_spec.js (renamed from spec/frontend/runner/components/cells/link_cell_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_actions_cell_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_owner_cell_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_status_cell_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js (renamed from spec/frontend/runner/components/cells/runner_summary_field_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js (renamed from spec/frontend/runner/components/registration/registration_dropdown_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js (renamed from spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js (renamed from spec/frontend/runner/components/registration/registration_token_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_assigned_item_spec.js (renamed from spec/frontend/runner/components/runner_assigned_item_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_checkbox_spec.js (renamed from spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js (renamed from spec/frontend/runner/components/runner_bulk_delete_spec.js)142
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js (renamed from spec/frontend/runner/components/runner_delete_button_spec.js)23
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_modal_spec.js (renamed from spec/frontend/runner/components/runner_delete_modal_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js (renamed from spec/frontend/runner/components/runner_details_spec.js)12
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_button_spec.js (renamed from spec/frontend/runner/components/runner_edit_button_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js (renamed from spec/frontend/runner/components/runner_filtered_search_bar_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/runner_groups_spec.js (renamed from spec/frontend/runner/components/runner_groups_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js (renamed from spec/frontend/runner/components/runner_header_spec.js)13
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_spec.js (renamed from spec/frontend/runner/components/runner_jobs_spec.js)14
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js (renamed from spec/frontend/runner/components/runner_jobs_table_spec.js)20
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js (renamed from spec/frontend/runner/components/runner_list_empty_state_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js (renamed from spec/frontend/runner/components/runner_list_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/runner_membership_toggle_spec.js (renamed from spec/frontend/runner/components/runner_membership_toggle_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_pagination_spec.js (renamed from spec/frontend/runner/components/runner_pagination_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js (renamed from spec/frontend/runner/components/runner_pause_button_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/runner_paused_badge_spec.js (renamed from spec/frontend/runner/components/runner_paused_badge_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_projects_spec.js (renamed from spec/frontend/runner/components/runner_projects_spec.js)14
-rw-r--r--spec/frontend/ci/runner/components/runner_status_badge_spec.js (renamed from spec/frontend/runner/components/runner_status_badge_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_status_popover_spec.js (renamed from spec/frontend/runner/components/runner_status_popover_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_tag_spec.js (renamed from spec/frontend/runner/components/runner_tag_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_tags_spec.js (renamed from spec/frontend/runner/components/runner_tags_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_type_badge_spec.js (renamed from spec/frontend/runner/components/runner_type_badge_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_type_tabs_spec.js (renamed from spec/frontend/runner/components/runner_type_tabs_spec.js)6
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js (renamed from spec/frontend/runner/components/runner_update_form_spec.js)14
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js (renamed from spec/frontend/runner/components/search_tokens/tag_token_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_count_spec.js (renamed from spec/frontend/runner/components/stat/runner_count_spec.js)12
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js (renamed from spec/frontend/runner/components/stat/runner_single_stat_spec.js)6
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_stats_spec.js (renamed from spec/frontend/runner/components/stat/runner_stats_spec.js)6
-rw-r--r--spec/frontend/ci/runner/graphql/local_state_spec.js (renamed from spec/frontend/runner/graphql/local_state_spec.js)8
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js (renamed from spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js)22
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js (renamed from spec/frontend/runner/group_runners/group_runners_app_spec.js)47
-rw-r--r--spec/frontend/ci/runner/local_storage_alert/save_alert_to_local_storage_spec.js (renamed from spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js)4
-rw-r--r--spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js (renamed from spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js)4
-rw-r--r--spec/frontend/ci/runner/mock_data.js (renamed from spec/frontend/runner/mock_data.js)24
-rw-r--r--spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js (renamed from spec/frontend/runner/runner_edit/runner_edit_app_spec.js)14
-rw-r--r--spec/frontend/ci/runner/runner_search_utils_spec.js (renamed from spec/frontend/runner/runner_search_utils_spec.js)2
-rw-r--r--spec/frontend/ci/runner/runner_update_form_utils_spec.js (renamed from spec/frontend/runner/runner_update_form_utils_spec.js)9
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js (renamed from spec/frontend/runner/sentry_utils_spec.js)4
-rw-r--r--spec/frontend/ci/runner/utils_spec.js (renamed from spec/frontend/runner/utils_spec.js)9
-rw-r--r--spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js175
-rw-r--r--spec/frontend/ci_variable_list/components/ci_group_variables_spec.js181
-rw-r--r--spec/frontend/ci_variable_list/components/ci_project_variables_spec.js202
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js90
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js48
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js2
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js428
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js119
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js323
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js38
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js86
-rw-r--r--spec/frontend/ci_variable_list/mocks.js77
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js319
-rw-r--r--spec/frontend/ci_variable_list/store/getters_spec.js21
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js136
-rw-r--r--spec/frontend/ci_variable_list/store/utils_spec.js49
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js1
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec.js9
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec_helper.js26
-rw-r--r--spec/frontend/content_editor/render_html_and_json_for_all_examples.js83
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js55
-rw-r--r--spec/frontend/content_editor/test_utils.js85
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js67
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js14
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap4
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js56
-rw-r--r--spec/frontend/diffs/mock_data/diff_file.js30
-rw-r--r--spec/frontend/diffs/store/actions_spec.js38
-rw-r--r--spec/frontend/diffs/store/utils_spec.js54
-rw-r--r--spec/frontend/diffs/utils/tree_worker_utils_spec.js64
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js71
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json2
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json6
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json6
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links.json (renamed from spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json)22
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json13
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json11
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml63
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml11
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml64
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_syntax_desc.yml (renamed from spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml)1
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml4
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml112
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml10
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js9
-rw-r--r--spec/frontend/environments/environment_actions_spec.js6
-rw-r--r--spec/frontend/environments/environment_rollback_spec.js2
-rw-r--r--spec/frontend/environments/graphql/mock_data.js1
-rw-r--r--spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js7
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb2
-rw-r--r--spec/frontend/fixtures/api_projects.rb2
-rw-r--r--spec/frontend/fixtures/application_settings.rb2
-rw-r--r--spec/frontend/fixtures/blob.rb2
-rw-r--r--spec/frontend/fixtures/branches.rb2
-rw-r--r--spec/frontend/fixtures/clusters.rb2
-rw-r--r--spec/frontend/fixtures/deploy_keys.rb2
-rw-r--r--spec/frontend/fixtures/freeze_period.rb2
-rw-r--r--spec/frontend/fixtures/integrations.rb2
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/job_artifacts.rb28
-rw-r--r--spec/frontend/fixtures/jobs.rb2
-rw-r--r--spec/frontend/fixtures/labels.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests.rb16
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb2
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb2
-rw-r--r--spec/frontend/fixtures/namespaces.rb20
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb13
-rw-r--r--spec/frontend/fixtures/pipelines.rb2
-rw-r--r--spec/frontend/fixtures/projects.rb2
-rw-r--r--spec/frontend/fixtures/prometheus_integration.rb2
-rw-r--r--spec/frontend/fixtures/raw.rb2
-rw-r--r--spec/frontend/fixtures/runner.rb4
-rw-r--r--spec/frontend/fixtures/snippet.rb2
-rw-r--r--spec/frontend/fixtures/static/gl_field_errors.html3
-rw-r--r--spec/frontend/fixtures/todos.rb2
-rw-r--r--spec/frontend/flash_spec.js14
-rw-r--r--spec/frontend/gfm_auto_complete/mock_data.js57
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js95
-rw-r--r--spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js102
-rw-r--r--spec/frontend/gitlab_version_check/index_spec.js116
-rw-r--r--spec/frontend/gl_field_errors_spec.js2
-rw-r--r--spec/frontend/google_cloud/service_accounts/list_spec.js29
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js48
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js56
-rw-r--r--spec/frontend/groups_projects/components/transfer_locations_spec.js377
-rw-r--r--spec/frontend/ide/components/ide_spec.js31
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js26
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js45
-rw-r--r--spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js214
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js27
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js86
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js45
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js76
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js57
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js60
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js2
-rw-r--r--spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js554
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js48
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js8
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js58
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js86
-rw-r--r--spec/frontend/issues/list/mock_data.js117
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js30
-rw-r--r--spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js20
-rw-r--r--spec/frontend/jobs/components/job/sidebar_spec.js72
-rw-r--r--spec/frontend/jobs/mock_data.js24
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js39
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js103
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js80
-rw-r--r--spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js37
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js28
-rw-r--r--spec/frontend/lib/utils/unit_format/index_spec.js6
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js90
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js5
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap223
-rw-r--r--spec/frontend/ml/experiment_tracking/components/experiment_spec.js44
-rw-r--r--spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js27
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js101
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js14
-rw-r--r--spec/frontend/notebook/cells/output/markdown_spec.js44
-rw-r--r--spec/frontend/notebook/mock_data.js2
-rw-r--r--spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap4
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js74
-rw-r--r--spec/frontend/notes/components/note_header_spec.js2
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js2
-rw-r--r--spec/frontend/notes/stores/getters_spec.js22
-rw-r--r--spec/frontend/observability/observability_app_spec.js73
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js60
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js57
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js71
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js152
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js43
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js104
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js34
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap54
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js33
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js158
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js78
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js17
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js280
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js75
-rw-r--r--spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js82
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap7
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js134
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js20
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js2
-rw-r--r--spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js161
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js26
-rw-r--r--spec/frontend/pipelines/mock_data.js4
-rw-r--r--spec/frontend/pipelines/pipeline_graph/utils_spec.js17
-rw-r--r--spec/frontend/pipelines/pipeline_tabs_spec.js32
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js16
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js6
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap2
-rw-r--r--spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap2
-rw-r--r--spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js2
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js68
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js24
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js47
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js5
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js11
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js268
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js4
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js37
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js45
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js20
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js42
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js151
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js1
-rw-r--r--spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap2
-rw-r--r--spec/frontend/reports/components/grouped_issues_list_spec.js2
-rw-r--r--spec/frontend/reports/components/report_item_spec.js4
-rw-r--r--spec/frontend/reports/grouped_test_report/components/modal_spec.js68
-rw-r--r--spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js96
-rw-r--r--spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js355
-rw-r--r--spec/frontend/reports/grouped_test_report/store/actions_spec.js168
-rw-r--r--spec/frontend/reports/grouped_test_report/store/mutations_spec.js162
-rw-r--r--spec/frontend/reports/grouped_test_report/store/utils_spec.js255
-rw-r--r--spec/frontend/runner/components/runner_stacked_layout_banner_spec.js41
-rw-r--r--spec/frontend/search/mock_data.js84
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js120
-rw-r--r--spec/frontend/search/sidebar/components/filters_spec.js132
-rw-r--r--spec/frontend/search/sidebar/components/scope_navigation_spec.js80
-rw-r--r--spec/frontend/search/store/actions_spec.js35
-rw-r--r--spec/frontend/search/store/mutations_spec.js22
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap16
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_form_spec.js10
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js6
-rw-r--r--spec/frontend/self_monitor/store/mutations_spec.js2
-rw-r--r--spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js93
-rw-r--r--spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js36
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_spec.js285
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js202
-rw-r--r--spec/frontend/token_access/mock_data.js12
-rw-r--r--spec/frontend/token_access/token_access_spec.js7
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js13
-rw-r--r--spec/frontend/users_select/utils_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js425
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap202
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js182
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js60
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js342
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js43
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js70
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js109
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js37
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js129
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js42
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js72
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js13
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/router_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js202
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js205
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/mock_data.js42
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/mock_data.js6
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js236
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js30
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js54
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js18
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js12
-rw-r--r--spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap453
-rw-r--r--spec/frontend/webhooks/components/form_url_app_spec.js125
-rw-r--r--spec/frontend/webhooks/components/form_url_mask_item_spec.js78
-rw-r--r--spec/frontend/webhooks/components/push_events_spec.js117
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_description_rendered_spec.js108
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js295
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js1
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js103
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js25
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js205
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js31
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js32
-rw-r--r--spec/frontend/work_items/mock_data.js118
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js39
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js11
-rw-r--r--spec/frontend/work_items/router_spec.js28
396 files changed, 12623 insertions, 7375 deletions
diff --git a/spec/frontend/__helpers__/raw_transformer.js b/spec/frontend/__helpers__/raw_transformer.js
new file mode 100644
index 00000000000..09101b7a64f
--- /dev/null
+++ b/spec/frontend/__helpers__/raw_transformer.js
@@ -0,0 +1,6 @@
+/* eslint-disable import/no-commonjs */
+module.exports = {
+ process: (content) => {
+ return `module.exports = ${JSON.stringify(content)}`;
+ },
+};
diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
index 2bd2b17a12d..42818c14029 100644
--- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
+++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
@@ -21,6 +21,7 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi
mindate="Mon Jul 06 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
placeholder="YYYY-MM-DD"
showclearbutton="true"
+ size="medium"
theme=""
/>
</gl-form-group-stub>
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
index b4af11169ad..e4313bdfa26 100644
--- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -73,7 +73,6 @@ describe('~/access_tokens/components/new_access_token_app', () => {
expect(InputCopyToggleVisibilityComponent.props('copyButtonTitle')).toBe(
sprintf(__('Copy %{accessTokenType}'), { accessTokenType }),
);
- expect(InputCopyToggleVisibilityComponent.props('initialVisibility')).toBe(true);
expect(InputCopyToggleVisibilityComponent.attributes('label')).toBe(
sprintf(__('Your new %{accessTokenType}'), { accessTokenType }),
);
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 411126d0c89..e6718f62b91 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal, GlLink } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { shallowMount, mount, createWrapper } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
@@ -36,6 +36,7 @@ describe('Signup Form', () => {
const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group');
const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group');
const findUserCapInput = () => wrapper.findByTestId('user-cap-input');
+ const findUserCapFormGroup = () => wrapper.findByTestId('user-cap-form-group');
const findModal = () => wrapper.findComponent(GlModal);
afterEach(() => {
@@ -214,4 +215,19 @@ describe('Signup Form', () => {
});
});
});
+
+ describe('rendering help links within user cap description', () => {
+ beforeEach(() => {
+ mountComponent({ mountFn: mount });
+ });
+
+ it('renders projectSharingHelpLink and groupSharingHelpLink', () => {
+ const [projectSharingLink, groupSharingLink] = findUserCapFormGroup().findAllComponents(
+ GlLink,
+ ).wrappers;
+
+ expect(projectSharingLink.attributes('href')).toBe(mockData.projectSharingHelpLink);
+ expect(groupSharingLink.attributes('href')).toBe(mockData.groupSharingHelpLink);
+ });
+ });
});
diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js
index 9e001e122a4..dd1ed317497 100644
--- a/spec/frontend/admin/signup_restrictions/mock_data.js
+++ b/spec/frontend/admin/signup_restrictions/mock_data.js
@@ -4,6 +4,7 @@ export const rawMockData = {
signupEnabled: 'true',
requireAdminApprovalAfterUserSignup: 'true',
sendUserConfirmationEmail: 'true',
+ emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
minimumPasswordLengthMax: '10',
@@ -22,6 +23,8 @@ export const rawMockData = {
passwordLowercaseRequired: 'true',
passwordUppercaseRequired: 'true',
passwordSymbolRequired: 'true',
+ projectSharingHelpLink: 'project-sharing/help/link',
+ groupSharingHelpLink: 'group-sharing/help/link',
};
export const mockData = {
@@ -30,6 +33,7 @@ export const mockData = {
signupEnabled: true,
requireAdminApprovalAfterUserSignup: true,
sendUserConfirmationEmail: true,
+ emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
minimumPasswordLengthMax: '10',
@@ -48,4 +52,6 @@ export const mockData = {
passwordLowercaseRequired: true,
passwordUppercaseRequired: true,
passwordSymbolRequired: true,
+ projectSharingHelpLink: 'project-sharing/help/link',
+ groupSharingHelpLink: 'group-sharing/help/link',
};
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index 4967753b91c..8e9652332c1 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -1,13 +1,13 @@
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Actions from '~/admin/users/components/actions';
+import Delete from '~/admin/users/components/actions/delete.vue';
import eventHub, {
EVENT_OPEN_DELETE_USER_MODAL,
} from '~/admin/users/components/modals/delete_user_modal_event_hub';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
-import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
-import { paths } from '../../mock_data';
+import { CONFIRMATION_ACTIONS } from '../../constants';
+import { paths, userDeletionObstacles } from '../../mock_data';
describe('Action components', () => {
let wrapper;
@@ -41,40 +41,33 @@ describe('Action components', () => {
});
});
- describe('DELETE_ACTION_COMPONENTS', () => {
+ describe('DELETE', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation();
});
- const userDeletionObstacles = [
- { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
- { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
- ];
-
- it.each(DELETE_ACTIONS)(
- 'renders a dropdown item that opens the delete user modal when clicked for "%s"',
- async (action) => {
- initComponent({
- component: Actions[capitalizeFirstCharacter(action)],
- props: {
- username: 'John Doe',
- paths,
- userDeletionObstacles,
- },
- });
+ it('renders a dropdown item that opens the delete user modal when Delete is clicked', async () => {
+ initComponent({
+ component: Delete,
+ props: {
+ username: 'John Doe',
+ userId: 1,
+ paths,
+ userDeletionObstacles,
+ },
+ });
- await findDropdownItem().vm.$emit('click');
+ await findDropdownItem().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith(
- EVENT_OPEN_DELETE_USER_MODAL,
- expect.objectContaining({
- username: 'John Doe',
- blockPath: paths.block,
- deletePath: paths[action],
- userDeletionObstacles,
- }),
- );
- },
- );
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ EVENT_OPEN_DELETE_USER_MODAL,
+ expect.objectContaining({
+ username: 'John Doe',
+ blockPath: paths.block,
+ deletePath: paths.delete,
+ userDeletionObstacles,
+ }),
+ );
+ });
});
});
diff --git a/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
new file mode 100644
index 00000000000..64a88aab2c2
--- /dev/null
+++ b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
@@ -0,0 +1,107 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeleteWithContributions from '~/admin/users/components/actions/delete_with_contributions.vue';
+import eventHub, {
+ EVENT_OPEN_DELETE_USER_MODAL,
+} from '~/admin/users/components/modals/delete_user_modal_event_hub';
+import { associationsCount } from '~/api/user_api';
+import {
+ paths,
+ associationsCount as associationsCountData,
+ userDeletionObstacles,
+} from '../../mock_data';
+
+jest.mock('~/admin/users/components/modals/delete_user_modal_event_hub', () => ({
+ ...jest.requireActual('~/admin/users/components/modals/delete_user_modal_event_hub'),
+ __esModule: true,
+ default: {
+ $emit: jest.fn(),
+ },
+}));
+
+jest.mock('~/api/user_api', () => ({
+ associationsCount: jest.fn(),
+}));
+
+describe('DeleteWithContributions', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ username: 'John Doe',
+ userId: 1,
+ paths,
+ userDeletionObstacles,
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(DeleteWithContributions, { propsData: defaultPropsData });
+ };
+
+ describe('when action is clicked', () => {
+ describe('when API request is loading', () => {
+ beforeEach(() => {
+ associationsCount.mockReturnValueOnce(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ it('displays loading icon and disables button', async () => {
+ await wrapper.trigger('click');
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findByRole('menuitem').attributes()).toMatchObject({
+ disabled: 'disabled',
+ 'aria-busy': 'true',
+ });
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(() => {
+ associationsCount.mockResolvedValueOnce({
+ data: associationsCountData,
+ });
+
+ createComponent();
+ });
+
+ it('emits event with association counts', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+
+ expect(associationsCount).toHaveBeenCalledWith(defaultPropsData.userId);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ EVENT_OPEN_DELETE_USER_MODAL,
+ expect.objectContaining({
+ associationsCount: associationsCountData,
+ username: defaultPropsData.username,
+ blockPath: paths.block,
+ deletePath: paths.deleteWithContributions,
+ userDeletionObstacles,
+ }),
+ );
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ beforeEach(() => {
+ associationsCount.mockRejectedValueOnce();
+
+ createComponent();
+ });
+
+ it('emits event with error', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ EVENT_OPEN_DELETE_USER_MODAL,
+ expect.objectContaining({
+ associationsCount: new Error(),
+ }),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap
new file mode 100644
index 00000000000..4237685e45c
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AssociationsListItem renders interpolated message in a \`li\` element 1`] = `"<li><strong>5</strong> groups</li>"`;
diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap
new file mode 100644
index 00000000000..dc98d367af7
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AssociationsList when counts are 0 does not render items 1`] = `""`;
+
+exports[`AssociationsList when counts are plural renders plural counts 1`] = `
+"<ul class=\\"gl-mb-5\\">
+ <li><strong>2</strong> groups</li>
+ <li><strong>3</strong> projects</li>
+ <li><strong>4</strong> issues</li>
+ <li><strong>5</strong> merge requests</li>
+</ul>"
+`;
+
+exports[`AssociationsList when counts are singular renders singular counts 1`] = `
+"<ul class=\\"gl-mb-5\\">
+ <li><strong>1</strong> group</li>
+ <li><strong>1</strong> project</li>
+ <li><strong>1</strong> issue</li>
+ <li><strong>1</strong> merge request</li>
+</ul>"
+`;
+
+exports[`AssociationsList when there is an error displays an alert 1`] = `
+"<div class=\\"gl-mb-5 gl-alert gl-alert-not-dismissible gl-alert-danger\\"><svg data-testid=\\"error-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"gl-icon s16 gl-alert-icon gl-alert-icon-no-title\\">
+ <use href=\\"#error\\"></use>
+ </svg>
+ <div role=\\"alert\\" aria-live=\\"assertive\\" class=\\"gl-alert-content\\">
+ <!---->
+ <div class=\\"gl-alert-body\\">An error occurred while fetching this user's contributions, and the request cannot return the number of issues, merge requests, groups, and projects linked to this user. If you proceed with deleting the user, all their contributions will still be deleted.</div>
+ <!---->
+ </div>
+ <!---->
+</div>"
+`;
diff --git a/spec/frontend/admin/users/components/associations/associations_list_item_spec.js b/spec/frontend/admin/users/components/associations/associations_list_item_spec.js
new file mode 100644
index 00000000000..5126df12c24
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/associations_list_item_spec.js
@@ -0,0 +1,25 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import AssociationsListItem from '~/admin/users/components/associations/associations_list_item.vue';
+import { n__ } from '~/locale';
+
+describe('AssociationsListItem', () => {
+ let wrapper;
+ const count = 5;
+
+ const createComponent = () => {
+ wrapper = mountExtended(AssociationsListItem, {
+ propsData: {
+ message: n__('%{count} group', '%{count} groups', count),
+ count,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders interpolated message in a `li` element', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/admin/users/components/associations/associations_list_spec.js b/spec/frontend/admin/users/components/associations/associations_list_spec.js
new file mode 100644
index 00000000000..d77a645111f
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/associations_list_spec.js
@@ -0,0 +1,78 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import AssociationsList from '~/admin/users/components/associations/associations_list.vue';
+
+describe('AssociationsList', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ associationsCount: {
+ groups_count: 1,
+ projects_count: 1,
+ issues_count: 1,
+ merge_requests_count: 1,
+ },
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(AssociationsList, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('when there is an error', () => {
+ it('displays an alert', () => {
+ createComponent({
+ propsData: {
+ associationsCount: new Error(),
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('when counts are singular', () => {
+ it('renders singular counts', () => {
+ createComponent();
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('when counts are plural', () => {
+ it('renders plural counts', () => {
+ createComponent({
+ propsData: {
+ associationsCount: {
+ groups_count: 2,
+ projects_count: 3,
+ issues_count: 4,
+ merge_requests_count: 5,
+ },
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('when counts are 0', () => {
+ it('does not render items', () => {
+ createComponent({
+ propsData: {
+ associationsCount: {
+ groups_count: 0,
+ projects_count: 0,
+ issues_count: 0,
+ merge_requests_count: 0,
+ },
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index 70ed9eeb3e1..2e892e292d7 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -1,10 +1,12 @@
import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import eventHub, {
EVENT_OPEN_DELETE_USER_MODAL,
} from '~/admin/users/components/modals/delete_user_modal_event_hub';
import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+import AssociationsList from '~/admin/users/components/associations/associations_list.vue';
import ModalStub from './stubs/modal_stub';
const TEST_DELETE_USER_URL = 'delete-url';
@@ -200,4 +202,24 @@ describe('Delete user modal', () => {
expect(obstacles.props('obstacles')).toEqual(userDeletionObstacles);
});
});
+
+ it('renders `AssociationsList` component and passes `associationsCount` prop', async () => {
+ const associationsCount = {
+ groups_count: 5,
+ projects_count: 0,
+ issues_count: 5,
+ merge_requests_count: 5,
+ };
+
+ createComponent();
+ emitOpenModalEvent({
+ ...mockModalData,
+ associationsCount,
+ });
+ await nextTick();
+
+ expect(wrapper.findComponent(AssociationsList).props('associationsCount')).toEqual(
+ associationsCount,
+ );
+ });
});
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index ffc05e744c8..1b080b05c95 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -121,8 +121,11 @@ describe('AdminUserActions component', () => {
it.each(DELETE_ACTIONS)('renders a delete action component item for "%s"', (action) => {
const component = wrapper.findComponent(Actions[capitalizeFirstCharacter(action)]);
- expect(component.props('username')).toBe(user.name);
- expect(component.props('paths')).toEqual(userPaths);
+ expect(component.props()).toMatchObject({
+ username: user.name,
+ userId: user.id,
+ paths: userPaths,
+ });
expect(component.text()).toBe(I18N_USER_ACTIONS[action]);
});
});
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index 73fa73c0b47..193ac3fa043 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -1,3 +1,5 @@
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+
export const users = [
{
id: 2177,
@@ -48,3 +50,15 @@ export const createGroupCountResponse = (groupCounts) => ({
},
},
});
+
+export const associationsCount = {
+ groups_count: 5,
+ projects_count: 5,
+ issues_count: 5,
+ merge_requests_count: 5,
+};
+
+export const userDeletionObstacles = [
+ { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
+ { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
+];
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
index 7a09fe3319d..562e86529ee 100644
--- a/spec/frontend/analytics/shared/components/daterange_spec.js
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -77,7 +77,7 @@ describe('Daterange component', () => {
it('sets the tooltip', () => {
const tooltip = findDaterangePicker().props('tooltip');
expect(tooltip).toBe(
- 'Showing data for workflow items created in this date range. Date range limited to 30 days.',
+ 'Showing data for workflow items completed in this date range. Date range limited to 30 days.',
);
});
});
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index e14ead0b8eb..9de588a02aa 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -1,10 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
+import getGroupTransferLocationsResponse from 'test_fixtures/api/groups/transfer_locations.json';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
-import { updateGroup } from '~/api/groups_api';
+import { DEFAULT_PER_PAGE } from '~/api';
+import { updateGroup, getGroupTransferLocations } from '~/api/groups_api';
const mockApiVersion = 'v4';
const mockUrlRoot = '/gitlab';
+const mockGroupId = '99';
describe('GroupsApi', () => {
let originalGon;
@@ -27,7 +30,6 @@ describe('GroupsApi', () => {
});
describe('updateGroup', () => {
- const mockGroupId = '99';
const mockData = { attr: 'value' };
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}`;
@@ -43,4 +45,25 @@ describe('GroupsApi', () => {
expect(res.data).toMatchObject({ id: mockGroupId, ...mockData });
});
});
+
+ describe('getGroupTransferLocations', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ });
+
+ it('retrieves transfer locations from the correct URL and returns them in the response data', async () => {
+ const params = { page: 1 };
+ const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/transfer_locations`;
+
+ mock.onGet(expectedUrl).replyOnce(200, { data: getGroupTransferLocationsResponse });
+
+ await expect(getGroupTransferLocations(mockGroupId, params)).resolves.toMatchObject({
+ data: { data: getGroupTransferLocationsResponse },
+ });
+
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
+ params: { ...params, per_page: DEFAULT_PER_PAGE },
+ });
+ });
+ });
});
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index ee7194bdf5f..ba6b73e8c1a 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -1,7 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
-import { followUser, unfollowUser } from '~/api/user_api';
+import { followUser, unfollowUser, associationsCount } from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
+import { associationsCount as associationsCountData } from 'jest/admin/users/mock_data';
describe('~/api/user_api', () => {
let axiosMock;
@@ -47,4 +48,18 @@ describe('~/api/user_api', () => {
expect(axiosMock.history.post[0].url).toBe(expectedUrl);
});
});
+
+ describe('associationsCount', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/users/1/associations_count';
+ const expectedResponse = { data: associationsCountData };
+
+ axiosMock.onGet(expectedUrl).replyOnce(200, expectedResponse);
+
+ await expect(associationsCount(1)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.get[0].url).toBe(expectedUrl);
+ });
+ });
});
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/artifacts/components/artifact_row_spec.js
new file mode 100644
index 00000000000..dcc0d684f13
--- /dev/null
+++ b/spec/frontend/artifacts/components/artifact_row_spec.js
@@ -0,0 +1,67 @@
+import { GlBadge, GlButton, GlFriendlyWrap } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactRow from '~/artifacts/components/artifact_row.vue';
+
+describe('ArtifactRow component', () => {
+ let wrapper;
+
+ const artifact = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0];
+
+ const findName = () => wrapper.findByTestId('job-artifact-row-name');
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findSize = () => wrapper.findByTestId('job-artifact-row-size');
+ const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+
+ const createComponent = (mountFn = shallowMountExtended) => {
+ wrapper = mountFn(ArtifactRow, {
+ propsData: {
+ artifact,
+ isLoading: false,
+ isLastRow: false,
+ },
+ stubs: { GlBadge, GlButton, GlFriendlyWrap },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('artifact details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('displays the artifact name and type', () => {
+ expect(findName().text()).toContain(artifact.name);
+ expect(findBadge().text()).toBe(artifact.fileType.toLowerCase());
+ });
+
+ it('displays the artifact size', () => {
+ expect(findSize().text()).toBe(numberToHumanSize(artifact.size));
+ });
+
+ it('displays the download button as a link to the download path', () => {
+ expect(findDownloadButton().attributes('href')).toBe(artifact.downloadPath);
+ });
+
+ it('displays the delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('emits the delete event when the delete button is clicked', async () => {
+ expect(wrapper.emitted('delete')).toBeUndefined();
+
+ findDeleteButton().trigger('click');
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
new file mode 100644
index 00000000000..c6ad13462f9
--- /dev/null
+++ b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
@@ -0,0 +1,122 @@
+import { GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactRow from '~/artifacts/components/artifact_row.vue';
+import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
+import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants';
+import { createAlert } from '~/flash';
+
+jest.mock('~/flash');
+
+const { artifacts } = getJobArtifactsResponse.data.project.jobs.nodes[0];
+const refetchArtifacts = jest.fn();
+
+Vue.use(VueApollo);
+
+describe('ArtifactsTableRowDetails component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const createComponent = (
+ handlers = {
+ destroyArtifactMutation: jest.fn(),
+ },
+ ) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(ArtifactsTableRowDetails, {
+ apolloProvider: createMockApollo([
+ [destroyArtifactMutation, requestHandlers.destroyArtifactMutation],
+ ]),
+ propsData: {
+ artifacts,
+ refetchArtifacts,
+ queryVariables: {},
+ },
+ data() {
+ return { deletingArtifactId: null };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('passes correct props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('to the artifact rows', () => {
+ [0, 1, 2].forEach((index) => {
+ expect(wrapper.findAllComponents(ArtifactRow).at(index).props()).toMatchObject({
+ artifact: artifacts.nodes[index],
+ });
+ });
+ });
+ });
+
+ describe('when the artifact row emits the delete event', () => {
+ it('shows the artifact delete modal', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(false);
+
+ await wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+
+ expect(findModal().props('visible')).toBe(true);
+ expect(findModal().props('title')).toBe(I18N_MODAL_TITLE(artifacts.nodes[0].name));
+ });
+ });
+
+ describe('when the artifact delete modal emits its primary event', () => {
+ it('triggers the destroyArtifact GraphQL mutation', async () => {
+ createComponent();
+ await waitForPromises();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+
+ expect(requestHandlers.destroyArtifactMutation).toHaveBeenCalledWith({
+ id: artifacts.nodes[0].id,
+ });
+ });
+
+ it('displays a flash message and refetches artifacts when the mutation fails', async () => {
+ createComponent({
+ destroyArtifactMutation: jest.fn().mockRejectedValue(new Error('Error!')),
+ });
+ await waitForPromises();
+
+ expect(wrapper.emitted('refetch')).toBeUndefined();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_DESTROY_ERROR });
+ expect(wrapper.emitted('refetch')).toBeDefined();
+ });
+ });
+
+ describe('when the artifact delete modal is cancelled', () => {
+ it('does not trigger the destroyArtifact GraphQL mutation', async () => {
+ createComponent();
+ await waitForPromises();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('cancel');
+
+ expect(requestHandlers.destroyArtifactMutation).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
new file mode 100644
index 00000000000..131b4b99bb2
--- /dev/null
+++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
@@ -0,0 +1,341 @@
+import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination, GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
+import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ARCHIVE_FILE_TYPE, JOBS_PER_PAGE, I18N_FETCH_ERROR } from '~/artifacts/constants';
+import { totalArtifactsSizeForJob } from '~/artifacts/utils';
+import { createAlert } from '~/flash';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+describe('JobArtifactsTable component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
+ const findDetailsInRow = (i) =>
+ findTable().findAll('tbody tr').at(i).findComponent(ArtifactsTableRowDetails);
+
+ const findCount = () => wrapper.findByTestId('job-artifacts-count');
+ const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i);
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
+ const findSuccessfulJobStatus = () => findStatuses().at(0);
+ const findFailedJobStatus = () => findStatuses().at(1);
+
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findJobLink = () => findLinks().at(0);
+ const findPipelineLink = () => findLinks().at(1);
+ const findRefLink = () => findLinks().at(2);
+ const findCommitLink = () => findLinks().at(3);
+
+ const findSize = () => wrapper.findByTestId('job-artifacts-size');
+ const findCreated = () => wrapper.findByTestId('job-artifacts-created');
+
+ const findDownloadButton = () => wrapper.findByTestId('job-artifacts-download-button');
+ const findBrowseButton = () => wrapper.findByTestId('job-artifacts-browse-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button');
+ const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const setPage = async (page) => {
+ findPagination().vm.$emit('input', page);
+ await waitForPromises();
+ };
+
+ let enoughJobsToPaginate = [...getJobArtifactsResponse.data.project.jobs.nodes];
+ while (enoughJobsToPaginate.length <= JOBS_PER_PAGE) {
+ enoughJobsToPaginate = [
+ ...enoughJobsToPaginate,
+ ...getJobArtifactsResponse.data.project.jobs.nodes,
+ ];
+ }
+ const getJobArtifactsResponseThatPaginates = {
+ data: { project: { jobs: { nodes: enoughJobsToPaginate } } },
+ };
+
+ const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
+ const archiveArtifact = job.artifacts.nodes.find(
+ (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
+ );
+
+ const createComponent = (
+ handlers = {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ },
+ data = {},
+ ) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(JobArtifactsTable, {
+ apolloProvider: createMockApollo([
+ [getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
+ ]),
+ provide: { projectPath: 'project/path' },
+ data() {
+ return data;
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('when loading, shows a loading state', () => {
+ createComponent();
+
+ expect(findLoadingState().exists()).toBe(true);
+ });
+
+ it('on error, shows an alert', async () => {
+ createComponent({
+ getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')),
+ });
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_FETCH_ERROR });
+ });
+
+ it('with data, renders the table', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
+
+ describe('job details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('shows the artifact count', () => {
+ expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`);
+ });
+
+ it('shows the job status as an icon for a successful job', () => {
+ expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true);
+ expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false);
+ });
+
+ it('shows the job status as a badge for other job statuses', () => {
+ expect(findFailedJobStatus().findComponent(GlBadge).exists()).toBe(true);
+ expect(findFailedJobStatus().findComponent(CiIcon).exists()).toBe(false);
+ });
+
+ it('shows links to the job, pipeline, ref, and commit', () => {
+ expect(findJobLink().text()).toBe(job.name);
+ expect(findJobLink().attributes('href')).toBe(job.webPath);
+
+ expect(findPipelineLink().text()).toBe(`#${getIdFromGraphQLId(job.pipeline.id)}`);
+ expect(findPipelineLink().attributes('href')).toBe(job.pipeline.path);
+
+ expect(findRefLink().text()).toBe(job.refName);
+ expect(findRefLink().attributes('href')).toBe(job.refPath);
+
+ expect(findCommitLink().text()).toBe(job.shortSha);
+ expect(findCommitLink().attributes('href')).toBe(job.commitPath);
+ });
+
+ it('shows the total size of artifacts', () => {
+ expect(findSize().text()).toBe(totalArtifactsSizeForJob(job));
+ });
+
+ it('shows the created time', () => {
+ expect(findCreated().text()).toBe('5 years ago');
+ });
+
+ describe('row expansion', () => {
+ it('toggles the visibility of the row details', async () => {
+ expect(findDetailsRows().length).toBe(0);
+
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsRows().length).toBe(1);
+
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsRows().length).toBe(0);
+ });
+
+ it('expands and collapses jobs', async () => {
+ // both jobs start collapsed
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+
+ findCountAt(0).trigger('click');
+ await waitForPromises();
+
+ // first job is expanded, second row has its details
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+
+ findCountAt(1).trigger('click');
+ await waitForPromises();
+
+ // both jobs are expanded, each has details below it
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+ expect(findDetailsInRow(3).exists()).toBe(true);
+
+ findCountAt(0).trigger('click');
+ await waitForPromises();
+
+ // first job collapsed, second job expanded
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+ expect(findDetailsInRow(2).exists()).toBe(true);
+ });
+
+ it('keeps the job expanded when an artifact is deleted', async () => {
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+
+ findArtifactDeleteButton().trigger('click');
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(true);
+
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('download button', () => {
+ it('is a link to the download path for the archive artifact', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('href')).toBe(archiveArtifact.downloadPath);
+ });
+
+ it('is disabled when there is no download path', async () => {
+ const jobWithoutDownloadPath = {
+ ...job,
+ archive: { downloadPath: null },
+ };
+
+ createComponent(
+ { getJobArtifactsQuery: jest.fn() },
+ { jobArtifacts: [jobWithoutDownloadPath] },
+ );
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('browse button', () => {
+ it('is a link to the browse path for the job', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('href')).toBe(job.browseArtifactsPath);
+ });
+
+ it('is disabled when there is no browse path', async () => {
+ const jobWithoutBrowsePath = {
+ ...job,
+ browseArtifactsPath: null,
+ };
+
+ createComponent(
+ { getJobArtifactsQuery: jest.fn() },
+ { jobArtifacts: [jobWithoutBrowsePath] },
+ );
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('delete button', () => {
+ it('shows a disabled delete button for now (coming soon)', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDeleteButton().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('pagination', () => {
+ const { pageInfo } = getJobArtifactsResponse.data.project.jobs;
+
+ beforeEach(async () => {
+ createComponent(
+ {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates),
+ },
+ {
+ count: enoughJobsToPaginate.length,
+ pageInfo,
+ },
+ );
+
+ await waitForPromises();
+ });
+
+ it('renders pagination and passes page props', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toMatchObject({
+ value: wrapper.vm.pagination.currentPage,
+ prevPage: wrapper.vm.prevPage,
+ nextPage: wrapper.vm.nextPage,
+ });
+ });
+
+ it('updates query variables when going to previous page', () => {
+ return setPage(1).then(() => {
+ expect(wrapper.vm.queryVariables).toMatchObject({
+ projectPath: 'project/path',
+ nextPageCursor: undefined,
+ prevPageCursor: pageInfo.startCursor,
+ });
+ });
+ });
+
+ it('updates query variables when going to next page', () => {
+ return setPage(2).then(() => {
+ expect(wrapper.vm.queryVariables).toMatchObject({
+ lastPageSize: null,
+ nextPageCursor: pageInfo.endCursor,
+ prevPageCursor: '',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/graphql/cache_update_spec.js b/spec/frontend/artifacts/graphql/cache_update_spec.js
new file mode 100644
index 00000000000..4d610328298
--- /dev/null
+++ b/spec/frontend/artifacts/graphql/cache_update_spec.js
@@ -0,0 +1,67 @@
+import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { removeArtifactFromStore } from '~/artifacts/graphql/cache_update';
+
+describe('Artifact table cache updates', () => {
+ let store;
+
+ const cacheMock = {
+ project: {
+ jobs: {
+ nodes: [
+ { artifacts: { nodes: [{ id: 'foo' }] } },
+ { artifacts: { nodes: [{ id: 'bar' }] } },
+ ],
+ },
+ },
+ };
+
+ const query = getJobArtifactsQuery;
+ const variables = { fullPath: 'path/to/project' };
+
+ beforeEach(() => {
+ store = {
+ readQuery: jest.fn().mockReturnValue(cacheMock),
+ writeQuery: jest.fn(),
+ };
+ });
+
+ describe('removeArtifactFromStore', () => {
+ it('calls readQuery', () => {
+ removeArtifactFromStore(store, 'foo', query, variables);
+ expect(store.readQuery).toHaveBeenCalledWith({ query, variables });
+ });
+
+ it('writes the correct result in the cache', () => {
+ removeArtifactFromStore(store, 'foo', query, variables);
+ expect(store.writeQuery).toHaveBeenCalledWith({
+ query,
+ variables,
+ data: {
+ project: {
+ jobs: {
+ nodes: [{ artifacts: { nodes: [] } }, { artifacts: { nodes: [{ id: 'bar' }] } }],
+ },
+ },
+ },
+ });
+ });
+
+ it('does not remove an unknown artifact', () => {
+ removeArtifactFromStore(store, 'baz', query, variables);
+ expect(store.writeQuery).toHaveBeenCalledWith({
+ query,
+ variables,
+ data: {
+ project: {
+ jobs: {
+ nodes: [
+ { artifacts: { nodes: [{ id: 'foo' }] } },
+ { artifacts: { nodes: [{ id: 'bar' }] } },
+ ],
+ },
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
index 2b9442162aa..de0e5063e49 100644
--- a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
+++ b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
@@ -1,34 +1,127 @@
-import $ from 'jquery';
+import { createWrapper } from '@vue/test-utils';
+import { __ } from '~/locale';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import renderMermaid from '~/behaviors/markdown/render_sandboxed_mermaid';
+import renderMermaid, {
+ MAX_CHAR_LIMIT,
+ MAX_MERMAID_BLOCK_LIMIT,
+ LAZY_ALERT_SHOWN_CLASS,
+} from '~/behaviors/markdown/render_sandboxed_mermaid';
-describe('Render mermaid diagrams for Gitlab Flavoured Markdown', () => {
- it('Does something', () => {
- document.body.dataset.page = '';
- setHTMLFixture(`
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-7:3" class="code highlight js-syntax-highlight language-mermaid white" lang="mermaid" id="code-4">
- <code class="js-render-mermaid">
- <span id="LC1" class="line" lang="mermaid">graph TD;</span>
- <span id="LC2" class="line" lang="mermaid">A--&gt;B</span>
- <span id="LC3" class="line" lang="mermaid">A--&gt;C</span>
- <span id="LC4" class="line" lang="mermaid">B--&gt;D</span>
- <span id="LC5" class="line" lang="mermaid">C--&gt;D</span>
- </code>
- </pre>
- <copy-code>
- <button type="button" class="btn btn-default btn-md gl-button btn-icon has-tooltip" data-title="Copy to clipboard" data-clipboard-target="pre#code-4">
- <svg><use xlink:href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#copy-to-clipboard"></use></svg>
- </button>
- </copy-code>
- </div>`);
- const els = $('pre.js-syntax-highlight').find('.js-render-mermaid');
-
- renderMermaid(els);
+describe('Mermaid diagrams renderer', () => {
+ // Finders
+ const findMermaidIframes = () => document.querySelectorAll('iframe[src="/-/sandbox/mermaid"]');
+ const findDangerousMermaidAlert = () =>
+ createWrapper(document.querySelector('[data-testid="alert-warning"]'));
+ // Helpers
+ const renderDiagrams = () => {
+ renderMermaid([...document.querySelectorAll('.js-render-mermaid')]);
jest.runAllTimers();
- expect(document.querySelector('pre.js-syntax-highlight').classList).toContain('gl-sr-only');
+ };
+
+ beforeEach(() => {
+ document.body.dataset.page = '';
+ });
+ afterEach(() => {
resetHTMLFixture();
});
+
+ it('renders a mermaid diagram', () => {
+ setHTMLFixture('<pre><code class="js-render-mermaid"></code></pre>');
+
+ expect(findMermaidIframes()).toHaveLength(0);
+
+ renderDiagrams();
+
+ expect(document.querySelector('pre').classList).toContain('gl-sr-only');
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+
+ describe('within a details element', () => {
+ beforeEach(() => {
+ setHTMLFixture('<details><pre><code class="js-render-mermaid"></code></pre></details>');
+ renderDiagrams();
+ });
+
+ it('does not render the diagram on load', () => {
+ expect(findMermaidIframes()).toHaveLength(0);
+ });
+
+ it('render the diagram when the details element is opened', () => {
+ document.querySelector('details').setAttribute('open', true);
+ document.querySelector('details').dispatchEvent(new Event('toggle'));
+ jest.runAllTimers();
+
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+ });
+
+ describe('dangerous diagrams', () => {
+ describe(`when the diagram's source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
+ beforeEach(() => {
+ setHTMLFixture(
+ `<pre>
+ <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT + 1)
+ .fill('a')
+ .join('')}</code>
+ </pre>`,
+ );
+ renderDiagrams();
+ });
+ it('does not render the diagram on load', () => {
+ expect(findMermaidIframes()).toHaveLength(0);
+ });
+
+ it('shows a warning about performance impact when rendering the diagram', () => {
+ expect(document.querySelector('pre').classList).toContain(LAZY_ALERT_SHOWN_CLASS);
+ expect(findDangerousMermaidAlert().exists()).toBe(true);
+ expect(findDangerousMermaidAlert().text()).toContain(
+ __('Warning: Displaying this diagram might cause performance issues on this page.'),
+ );
+ });
+
+ it("renders the diagram when clicking on the alert's button", () => {
+ findDangerousMermaidAlert().find('button').trigger('click');
+ jest.runAllTimers();
+
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+ });
+
+ it(`stops rendering diagrams once the total rendered source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
+ setHTMLFixture(
+ `<pre>
+ <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT - 1)
+ .fill('a')
+ .join('')}</code>
+ <code class="js-render-mermaid">2</code>
+ <code class="js-render-mermaid">3</code>
+ <code class="js-render-mermaid">4</code>
+ </pre>`,
+ );
+ renderDiagrams();
+
+ expect(findMermaidIframes()).toHaveLength(3);
+ });
+
+ // Note: The test case below is provided for convenience but should remain skipped as the DOM
+ // operations it requires are too expensive and would significantly slow down the test suite.
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip(`stops rendering diagrams when the rendered diagrams count exceeds ${MAX_MERMAID_BLOCK_LIMIT}`, () => {
+ setHTMLFixture(
+ `<pre>
+ ${Array(MAX_MERMAID_BLOCK_LIMIT + 1)
+ .fill('<code class="js-render-mermaid"></code>')
+ .join('')}
+ </pre>`,
+ );
+ renderDiagrams();
+
+ expect([...document.querySelectorAll('.js-render-mermaid')]).toHaveLength(
+ MAX_MERMAID_BLOCK_LIMIT + 1,
+ );
+ expect(findMermaidIframes()).toHaveLength(MAX_MERMAID_BLOCK_LIMIT);
+ });
+ });
});
diff --git a/spec/frontend/blob/blob_blame_link_spec.js b/spec/frontend/blob/blob_blame_link_spec.js
index 060e8803520..18adeed1f02 100644
--- a/spec/frontend/blob/blob_blame_link_spec.js
+++ b/spec/frontend/blob/blob_blame_link_spec.js
@@ -1,5 +1,5 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import addBlameLink from '~/blob/blob_blame_link';
+import { addBlameLink } from '~/blob/blob_blame_link';
describe('Blob links', () => {
const mouseoverEvent = new MouseEvent('mouseover', {
@@ -10,9 +10,10 @@ describe('Blob links', () => {
beforeEach(() => {
setHTMLFixture(`
- <div id="blob-content-holder">
+ <div id="blob-content-holder" class="js-per-page" data-blame-per-page="1000">
<div class="line-numbers" data-blame-path="/blamePath">
<a id="L5" href="#L5" data-line-number="5" class="file-line-num js-line-links">5</a>
+ <a id="L1005" href="#L1005" data-line-number="1005" class="file-line-num js-line-links">1005</a>
</div>
<pre id="LC5">Line 5 content</pre>
</div>
@@ -44,4 +45,11 @@ describe('Blob links', () => {
expect(lineLink).not.toBeNull();
expect(lineLink.getAttribute('href')).toBe('#L5');
});
+
+ it('adds page parameter when needed', () => {
+ document.querySelectorAll('.file-line-num')[1].dispatchEvent(mouseoverEvent);
+ const blameLink = document.querySelectorAll('.file-line-blame')[1];
+ expect(blameLink).not.toBeNull();
+ expect(blameLink.getAttribute('href')).toBe('/blamePath?page=2#L1005');
+ });
});
diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap
deleted file mode 100644
index 72761c18b3d..00000000000
--- a/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Blob Header Editing rendering matches the snapshot 1`] = `
-<div
- class="file-content code"
->
- <div
- data-editor-loading=""
- id="editor"
- >
- <pre
- class="editor-loading-content"
- >
- Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- </pre>
- </div>
-</div>
-`;
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js
deleted file mode 100644
index 5017b624292..00000000000
--- a/spec/frontend/blob/components/blob_edit_content_spec.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import BlobEditContent from '~/blob/components/blob_edit_content.vue';
-import * as utils from '~/blob/utils';
-
-jest.mock('~/editor/source_editor');
-
-describe('Blob Header Editing', () => {
- let wrapper;
- const onDidChangeModelContent = jest.fn();
- const updateModelLanguage = jest.fn();
- const getValue = jest.fn();
- const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
- const fileName = 'lorem.txt';
- const fileGlobalId = 'snippet_777';
-
- function createComponent(props = {}) {
- wrapper = shallowMount(BlobEditContent, {
- propsData: {
- value,
- fileName,
- fileGlobalId,
- ...props,
- },
- });
- }
-
- beforeEach(() => {
- jest.spyOn(utils, 'initSourceEditor').mockImplementation(() => ({
- onDidChangeModelContent,
- updateModelLanguage,
- getValue,
- dispose: jest.fn(),
- }));
-
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const triggerChangeContent = (val) => {
- getValue.mockReturnValue(val);
- const [cb] = onDidChangeModelContent.mock.calls[0];
-
- cb();
-
- jest.runOnlyPendingTimers();
- };
-
- describe('rendering', () => {
- it('matches the snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders content', () => {
- expect(wrapper.text()).toContain(value);
- });
- });
-
- describe('functionality', () => {
- it('does not fail without content', () => {
- const spy = jest.spyOn(global.console, 'error');
- createComponent({ value: undefined });
-
- expect(spy).not.toHaveBeenCalled();
- expect(wrapper.find('#editor').exists()).toBe(true);
- });
-
- it('initialises Source Editor', () => {
- const el = wrapper.findComponent({ ref: 'editor' }).element;
- expect(utils.initSourceEditor).toHaveBeenCalledWith({
- el,
- blobPath: fileName,
- blobGlobalId: fileGlobalId,
- blobContent: value,
- });
- });
-
- it('reacts to the changes in fileName', () => {
- const newFileName = 'ipsum.txt';
-
- wrapper.setProps({
- fileName: newFileName,
- });
-
- return nextTick().then(() => {
- expect(updateModelLanguage).toHaveBeenCalledWith(newFileName);
- });
- });
-
- it('registers callback with editor onChangeContent', () => {
- expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
- });
-
- it('emits input event when the blob content is changed', () => {
- expect(wrapper.emitted().input).toBeUndefined();
-
- triggerChangeContent(value);
-
- expect(wrapper.emitted().input).toEqual([[value]]);
- });
- });
-});
diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js
index a543c0060cb..24f70acb093 100644
--- a/spec/frontend/blob/utils_spec.js
+++ b/spec/frontend/blob/utils_spec.js
@@ -1,44 +1,32 @@
import * as utils from '~/blob/utils';
-import Editor from '~/editor/source_editor';
-
-jest.mock('~/editor/source_editor');
describe('Blob utilities', () => {
- describe('initSourceEditor', () => {
- let editorEl;
- const blobPath = 'foo.txt';
- const blobContent = 'Foo bar';
- const blobGlobalId = 'snippet_777';
-
- beforeEach(() => {
- editorEl = document.createElement('div');
+ describe('getPageParamValue', () => {
+ it('returns empty string if no perPage parameter is provided', () => {
+ const pageParamValue = utils.getPageParamValue(5);
+ expect(pageParamValue).toEqual('');
});
-
- describe('Monaco editor', () => {
- it('initializes the Source Editor', () => {
- utils.initSourceEditor({ el: editorEl });
- expect(Editor).toHaveBeenCalledWith({
- scrollbar: {
- alwaysConsumeMouseWheel: false,
- },
- });
- });
-
- it.each([[{}], [{ blobPath, blobContent, blobGlobalId }]])(
- 'creates the instance with the passed parameters %s',
- (extraParams) => {
- const params = {
- el: editorEl,
- ...extraParams,
- };
-
- expect(Editor.prototype.createInstance).not.toHaveBeenCalled();
-
- utils.initSourceEditor(params);
-
- expect(Editor.prototype.createInstance).toHaveBeenCalledWith(params);
- },
- );
+ it('returns empty string if page is equal 1', () => {
+ const pageParamValue = utils.getPageParamValue(1000, 1000);
+ expect(pageParamValue).toEqual('');
+ });
+ it('returns correct page parameter value', () => {
+ const pageParamValue = utils.getPageParamValue(1001, 1000);
+ expect(pageParamValue).toEqual(2);
+ });
+ it('accepts strings as a parameter and returns correct result', () => {
+ const pageParamValue = utils.getPageParamValue('1001', '1000');
+ expect(pageParamValue).toEqual(2);
+ });
+ });
+ describe('getPageSearchString', () => {
+ it('returns empty search string if page parameter is empty value', () => {
+ const path = utils.getPageSearchString('/blamePath', '');
+ expect(path).toEqual('');
+ });
+ it('returns correct search string if value is provided', () => {
+ const searchString = utils.getPageSearchString('/blamePath', 3);
+ expect(searchString).toEqual('?page=3');
});
});
});
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index c031cae11df..dda46e97b85 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -1,3 +1,4 @@
+import MockAdapter from 'axios-mock-adapter';
import { Emitter } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
@@ -8,6 +9,7 @@ import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markd
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/editor/source_editor');
jest.mock('~/editor/extensions/source_editor_extension_base');
@@ -32,6 +34,7 @@ const markdownExtensions = [
describe('Blob Editing', () => {
let blobInstance;
+ let mock;
const useMock = jest.fn(() => markdownExtensions);
const unuseMock = jest.fn();
const emitter = new Emitter();
@@ -44,7 +47,10 @@ describe('Blob Editing', () => {
onDidChangeModelLanguage: emitter.event,
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
setHTMLFixture(`
+ <div class="js-edit-mode-pane"></div>
+ <div class="js-edit-mode"><a href="#write">Write</a><a href="#preview">Preview</a></div>
<form class="js-edit-blob-form">
<div id="file_path"></div>
<div id="editor"></div>
@@ -54,6 +60,7 @@ describe('Blob Editing', () => {
jest.spyOn(SourceEditor.prototype, 'createInstance').mockReturnValue(mockInstance);
});
afterEach(() => {
+ mock.restore();
jest.clearAllMocks();
unuseMock.mockClear();
useMock.mockClear();
@@ -108,6 +115,47 @@ describe('Blob Editing', () => {
});
});
+ describe('correctly handles toggling the live-preview panel for different file types', () => {
+ it.each`
+ desc | isMarkdown | isPreviewOpened | tabToClick | shouldOpenPreview | shouldClosePreview | expectedDesc
+ ${'not markdown with preview closed'} | ${false} | ${false} | ${'#write'} | ${false} | ${false} | ${'not toggle preview'}
+ ${'not markdown with preview closed'} | ${false} | ${false} | ${'#preview'} | ${false} | ${false} | ${'not toggle preview'}
+ ${'markdown with preview closed'} | ${true} | ${false} | ${'#write'} | ${false} | ${false} | ${'not toggle preview'}
+ ${'markdown with preview closed'} | ${true} | ${false} | ${'#preview'} | ${true} | ${false} | ${'open preview'}
+ ${'markdown with preview opened'} | ${true} | ${true} | ${'#write'} | ${false} | ${true} | ${'close preview'}
+ ${'markdown with preview opened'} | ${true} | ${true} | ${'#preview'} | ${false} | ${false} | ${'not toggle preview'}
+ `(
+ 'when $desc, clicking $tabToClick should $expectedDesc',
+ async ({
+ isMarkdown,
+ isPreviewOpened,
+ tabToClick,
+ shouldOpenPreview,
+ shouldClosePreview,
+ }) => {
+ const fire = jest.fn();
+ SourceEditor.prototype.createInstance = jest.fn().mockReturnValue({
+ ...mockInstance,
+ markdownPreview: {
+ eventEmitter: {
+ fire,
+ },
+ },
+ });
+ await initEditor(isMarkdown);
+ blobInstance.markdownLivePreviewOpened = isPreviewOpened;
+ const elToClick = document.querySelector(`a[href='${tabToClick}']`);
+ elToClick.dispatchEvent(new Event('click'));
+
+ if (shouldOpenPreview || shouldClosePreview) {
+ expect(fire).toHaveBeenCalled();
+ } else {
+ expect(fire).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
+
it('adds trailing newline to the blob content on submit', async () => {
const form = document.querySelector('.js-edit-blob-form');
const fileContentEl = document.getElementById('file-content');
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 3ebc51c4bcb..d05e057095d 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -7,7 +7,6 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
-import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
@@ -49,12 +48,11 @@ describe('Board card component', () => {
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
- const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition);
const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const performSearchMock = jest.fn();
- const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => {
+ const createStore = ({ isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
actions: {
@@ -67,13 +65,12 @@ describe('Board card component', () => {
},
getters: {
isGroupBoard: () => true,
- isEpicBoard: () => isEpicBoard,
isProjectBoard: () => isProjectBoard,
},
});
};
- const createWrapper = (props = {}) => {
+ const createWrapper = ({ props = {}, isEpicBoard = false } = {}) => {
wrapper = mountExtended(BoardCardInner, {
store,
propsData: {
@@ -99,6 +96,7 @@ describe('Board card component', () => {
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
+ isEpicBoard,
},
});
};
@@ -113,7 +111,7 @@ describe('Board card component', () => {
};
createStore();
- createWrapper({ item: issue, list });
+ createWrapper({ props: { item: issue, list } });
});
afterEach(() => {
@@ -143,16 +141,12 @@ describe('Board card component', () => {
expect(findHiddenIssueIcon().exists()).toBe(false);
});
- it('renders the move to position icon', () => {
- expect(findMoveToPositionComponent().exists()).toBe(true);
- });
-
it('does not render the work type icon by default', () => {
expect(findWorkItemIcon().exists()).toBe(false);
});
it('renders the work type icon when props is passed', () => {
- createWrapper({ item: issue, list, showWorkItemTypeIcon: true });
+ createWrapper({ props: { item: issue, list, showWorkItemTypeIcon: true } });
expect(findWorkItemIcon().exists()).toBe(true);
expect(findWorkItemIcon().props('workItemType')).toBe(issue.type);
});
@@ -183,9 +177,11 @@ describe('Board card component', () => {
describe('blocked', () => {
it('renders blocked icon if issue is blocked', async () => {
createWrapper({
- item: {
- ...issue,
- blocked: true,
+ props: {
+ item: {
+ ...issue,
+ blocked: true,
+ },
},
});
@@ -194,9 +190,11 @@ describe('Board card component', () => {
it('does not show blocked icon if issue is not blocked', () => {
createWrapper({
- item: {
- ...issue,
- blocked: false,
+ props: {
+ item: {
+ ...issue,
+ blocked: false,
+ },
},
});
@@ -207,9 +205,11 @@ describe('Board card component', () => {
describe('confidential issue', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- confidential: true,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ confidential: true,
+ },
},
});
});
@@ -222,9 +222,11 @@ describe('Board card component', () => {
describe('hidden issue', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- hidden: true,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ hidden: true,
+ },
},
});
});
@@ -247,11 +249,13 @@ describe('Board card component', () => {
describe('with avatar', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees: [user],
- updateData(newData) {
- Object.assign(this, newData);
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees: [user],
+ updateData(newData) {
+ Object.assign(this, newData);
+ },
},
},
});
@@ -300,15 +304,17 @@ describe('Board card component', () => {
global.gon.default_avatar_url = 'default_avatar';
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees: [
- {
- id: 1,
- name: 'testing 123',
- username: 'test',
- },
- ],
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees: [
+ {
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ },
+ ],
+ },
},
});
});
@@ -329,28 +335,30 @@ describe('Board card component', () => {
describe('multiple assignees', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees: [
- {
- id: 2,
- name: 'user2',
- username: 'user2',
- avatarUrl: 'test_image',
- },
- {
- id: 3,
- name: 'user3',
- username: 'user3',
- avatarUrl: 'test_image',
- },
- {
- id: 4,
- name: 'user4',
- username: 'user4',
- avatarUrl: 'test_image',
- },
- ],
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees: [
+ {
+ id: 2,
+ name: 'user2',
+ username: 'user2',
+ avatarUrl: 'test_image',
+ },
+ {
+ id: 3,
+ name: 'user3',
+ username: 'user3',
+ avatarUrl: 'test_image',
+ },
+ {
+ id: 4,
+ name: 'user4',
+ username: 'user4',
+ avatarUrl: 'test_image',
+ },
+ ],
+ },
},
});
});
@@ -370,9 +378,11 @@ describe('Board card component', () => {
});
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees,
+ },
},
});
});
@@ -396,9 +406,11 @@ describe('Board card component', () => {
})),
];
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees,
+ },
},
});
@@ -411,7 +423,7 @@ describe('Board card component', () => {
describe('labels', () => {
beforeEach(() => {
- createWrapper({ item: { ...issue, labels: [list.label, label1] } });
+ createWrapper({ props: { item: { ...issue, labels: [list.label, label1] } } });
});
it('does not render list label but renders all other labels', () => {
@@ -423,7 +435,7 @@ describe('Board card component', () => {
});
it('does not render label if label does not have an ID', async () => {
- createWrapper({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
+ createWrapper({ props: { item: { ...issue, labels: [label1, { title: 'closed' }] } } });
await nextTick();
@@ -435,11 +447,13 @@ describe('Board card component', () => {
describe('filterByLabel method', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...issue,
- labels: [label1],
+ props: {
+ item: {
+ ...issue,
+ labels: [label1],
+ },
+ updateFilters: true,
},
- updateFilters: true,
});
});
@@ -486,9 +500,11 @@ describe('Board card component', () => {
describe('loading', () => {
it('renders loading icon', async () => {
createWrapper({
- item: {
- ...issue,
- isLoading: true,
+ props: {
+ item: {
+ ...issue,
+ isLoading: true,
+ },
},
});
@@ -510,17 +526,20 @@ describe('Board card component', () => {
};
beforeEach(() => {
- createStore({ isEpicBoard: true });
+ createStore();
});
it('should render if the item has issues', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum,
- hasIssues: true,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts,
+ descendantWeightSum,
+ hasIssues: true,
+ },
},
+ isEpicBoard: true,
});
expect(findEpicCountables().exists()).toBe(true);
@@ -541,18 +560,21 @@ describe('Board card component', () => {
it('shows render item countBadge, weights, and progress correctly', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
- },
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts: {
+ ...descendantCounts,
+ openedIssues: 1,
+ },
+ descendantWeightSum: {
+ closedIssues: 10,
+ openedIssues: 5,
+ },
+ hasIssues: true,
},
- hasIssues: true,
},
+ isEpicBoard: true,
});
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
@@ -562,15 +584,18 @@ describe('Board card component', () => {
it('does not render progress when weight is zero', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts: {
+ ...descendantCounts,
+ openedIssues: 1,
+ },
+ descendantWeightSum,
+ hasIssues: true,
},
- descendantWeightSum,
- hasIssues: true,
},
+ isEpicBoard: true,
});
expect(findEpicBadgeProgress().exists()).toBe(false);
@@ -578,15 +603,18 @@ describe('Board card component', () => {
it('renders the tooltip with the correct data', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts,
+ descendantWeightSum: {
+ closedIssues: 10,
+ openedIssues: 5,
+ },
+ hasIssues: true,
},
- hasIssues: true,
},
+ isEpicBoard: true,
});
const tooltip = findEpicCountablesTotalTooltip();
@@ -595,10 +623,5 @@ describe('Board card component', () => {
expect(findEpicCountablesTotalWeight().text()).toBe('15');
expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed');
});
-
- it('does not render the move to position icon', () => {
- createWrapper();
- expect(findMoveToPositionComponent().exists()).toBe(false);
- });
});
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 65a41c49e7f..c5c3faf1712 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -101,6 +101,8 @@ export default function createComponent({
weightFeatureAvailable: false,
boardWeight: null,
canAdminList: true,
+ isIssueBoard: true,
+ isEpicBoard: false,
...provide,
},
stubs,
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 9b0c0b93ffb..3a2beb714e9 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createComponent from 'jest/boards/board_list_helper';
import BoardCard from '~/boards/components/board_card.vue';
import eventHub from '~/boards/eventhub';
+import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import { mockIssues } from './mock_data';
@@ -15,6 +16,7 @@ describe('Board list component', () => {
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]');
const findDraggable = () => wrapper.findComponent(Draggable);
+ const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition);
const startDrag = (
params = {
@@ -99,6 +101,10 @@ describe('Board list component', () => {
await nextTick();
expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
});
+
+ it('renders the move to position icon', () => {
+ expect(findMoveToPositionComponent().exists()).toBe(true);
+ });
});
describe('load more issues', () => {
diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js
index 5fae1c4359f..a3b2988ce75 100644
--- a/spec/frontend/boards/components/board_add_new_column_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_spec.js
@@ -53,11 +53,11 @@ describe('Board card layout', () => {
state: {
labels,
labelsLoading: false,
- isEpicBoard: false,
},
}),
provide: {
scopedLabelsAvailable: true,
+ isEpicBoard: false,
},
}),
);
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index dee097bfb08..c209f2f82e6 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -28,6 +28,7 @@ describe('BoardApp', () => {
store,
provide: {
...provide,
+ fullBoardId: 'gid://gitlab/Board/1',
},
});
};
diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js
index 7254b9486ef..8dee3c77787 100644
--- a/spec/frontend/boards/components/board_card_move_to_position_spec.js
+++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js
@@ -48,6 +48,7 @@ describe('Board Card Move to position', () => {
propsData: {
item: mockIssue2,
list: mockList,
+ listItemsLength: 3,
index: 0,
...propsData,
},
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 2feaa5dff8c..38b79e2e3f3 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -30,7 +30,6 @@ describe('Board card', () => {
},
actions: mockActions,
getters: {
- isEpicBoard: () => false,
isProjectBoard: () => false,
},
});
@@ -61,6 +60,7 @@ describe('Board card', () => {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
+ isEpicBoard: false,
...provide,
},
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 97d9e08f5d4..b2138700602 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,15 +1,20 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
+import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
-import { mockLists } from '../mock_data';
+import { mockLists, boardListsQueryResponse } from '../mock_data';
+Vue.use(VueApollo);
Vue.use(Vuex);
const actions = {
@@ -18,6 +23,7 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
+ let fakeApollo;
window.gon = {};
const defaultState = {
@@ -35,26 +41,68 @@ describe('BoardContent', () => {
});
};
- const createComponent = ({ state, props = {}, canAdminList = true } = {}) => {
+ const createComponent = ({
+ state,
+ props = {},
+ canAdminList = true,
+ isApolloBoard = false,
+ issuableType = 'issue',
+ isIssueBoard = true,
+ isEpicBoard = false,
+ boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
+ } = {}) => {
+ fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
+
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
+ apolloProvider: fakeApollo,
propsData: {
- lists: mockLists,
disabled: false,
+ boardId: 'gid://gitlab/Board/1',
...props,
},
provide: {
canAdminList,
+ boardType: 'group',
+ fullPath: 'gitlab-org/gitlab',
+ issuableType,
+ isIssueBoard,
+ isEpicBoard,
+ isApolloBoard,
},
store,
});
};
+ beforeAll(() => {
+ global.ResizeObserver = class MockResizeObserver {
+ constructor(callback) {
+ this.callback = callback;
+
+ this.entries = [];
+ }
+
+ observe(entry) {
+ this.entries.push(entry);
+ }
+
+ disconnect() {
+ this.entries = [];
+ this.callback = null;
+ }
+
+ trigger() {
+ this.callback(this.entries);
+ }
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
+ fakeApollo = null;
});
describe('default', () => {
@@ -74,11 +122,22 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(EpicsSwimlanes).exists()).toBe(false);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
+
+ it('resizes the list on resize', async () => {
+ window.innerHeight = 1000;
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
+
+ wrapper.vm.resizeObserver.trigger();
+
+ await nextTick();
+
+ expect(wrapper.findComponent({ ref: 'list' }).attributes('style')).toBe('height: 900px;');
+ });
});
describe('when issuableType is not issue', () => {
beforeEach(() => {
- createComponent({ state: { issuableType: 'foo' } });
+ createComponent({ issuableType: 'foo', isIssueBoard: false });
});
it('does not render BoardContentSidebar', () => {
@@ -105,4 +164,19 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(Draggable).exists()).toBe(false);
});
});
+
+ describe('when Apollo boards FF is on', () => {
+ beforeEach(async () => {
+ createComponent({ isApolloBoard: true });
+ await waitForPromises();
+ });
+
+ it('renders a BoardColumn component per list', () => {
+ expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
+ });
+
+ it('renders BoardContentSidebar', () => {
+ expect(wrapper.findComponent(BoardContentSidebar).exists()).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 1a07b9f0b78..6f17e4193a3 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -3,7 +3,19 @@ import Vue from 'vue';
import Vuex from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import * as urlUtility from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
+import {
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_HEALTH,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ 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 LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@@ -17,7 +29,7 @@ describe('BoardFilteredSearch', () => {
const tokens = [
{
icon: 'labels',
- title: __('Label'),
+ title: TOKEN_TITLE_LABEL,
type: 'label',
operators: [
{ value: '=', description: 'is' },
@@ -30,7 +42,7 @@ describe('BoardFilteredSearch', () => {
},
{
icon: 'pencil',
- title: __('Author'),
+ title: TOKEN_TITLE_AUTHOR,
type: 'author',
operators: [
{ value: '=', description: 'is' },
@@ -117,16 +129,16 @@ describe('BoardFilteredSearch', () => {
it('sets the url params to the correct results', async () => {
const mockFilters = [
- { type: 'author', value: { data: 'root', operator: '=' } },
- { type: 'assignee', value: { data: 'root', operator: '=' } },
- { type: 'label', value: { data: 'label', operator: '=' } },
- { type: 'label', value: { data: 'label&2', operator: '=' } },
- { type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
- { type: 'type', value: { data: 'INCIDENT', operator: '=' } },
- { type: 'weight', value: { data: '2', operator: '=' } },
- { type: 'iteration', value: { data: 'Any&3', operator: '=' } },
- { type: 'release', value: { data: 'v1.0.0', operator: '=' } },
- { type: 'health_status', value: { data: 'onTrack', operator: '=' } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'root', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label&2', operator: '=' } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'New Milestone', operator: '=' } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'INCIDENT', operator: '=' } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: '2', operator: '=' } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: 'Any&3', operator: '=' } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v1.0.0', operator: '=' } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: '=' } },
];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
@@ -170,9 +182,9 @@ describe('BoardFilteredSearch', () => {
it('passes the correct props to FilterSearchBar', () => {
expect(findFilteredSearch().props('initialFilterValue')).toEqual([
- { type: 'author', value: { data: 'root', operator: '=' } },
- { type: 'label', value: { data: 'label', operator: '=' } },
- { type: 'health_status', value: { data: 'Any', operator: '=' } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'Any', operator: '=' } },
]);
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 50901f3fe84..4633612891c 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -59,7 +59,6 @@ describe('Board List Header Component', () => {
store = new Vuex.Store({
state: {},
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
- getters: { isEpicBoard: () => false },
});
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
@@ -76,6 +75,7 @@ describe('Board List Header Component', () => {
boardId,
weightFeatureAvailable: false,
currentUserId,
+ isEpicBoard: false,
},
}),
);
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 4171a6236de..7d602042685 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -45,6 +45,7 @@ describe('BoardSettingsSidebar', () => {
provide: {
canAdminList,
scopedLabelsAvailable: false,
+ isIssueBoard: true,
},
directives: {
GlModal: createMockDirective(),
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index 997768a0cc7..08b5042f70f 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -15,18 +15,14 @@ describe('BoardTopBar', () => {
Vue.use(Vuex);
- const createStore = ({ mockGetters = {} } = {}) => {
+ const createStore = () => {
return new Vuex.Store({
state: {},
- getters: {
- isEpicBoard: () => false,
- ...mockGetters,
- },
});
};
- const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
- const store = createStore({ mockGetters });
+ const createComponent = ({ provide = {} } = {}) => {
+ const store = createStore();
wrapper = shallowMount(BoardTopBar, {
store,
provide: {
@@ -36,6 +32,7 @@ describe('BoardTopBar', () => {
fullPath: 'gitlab-org',
boardType: 'group',
releasesFetchPath: '/releases',
+ isIssueBoard: true,
...provide,
},
stubs: { IssueBoardFilteredSearch },
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index dc1f3246be0..3c26fa97338 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,7 +1,22 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
-import { __ } from '~/locale';
+import {
+ OPERATOR_IS_AND_IS_NOT,
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_TYPE,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ 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 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';
@@ -433,8 +448,11 @@ export const mockList = {
label: null,
assignee: null,
milestone: null,
+ iteration: null,
loading: false,
issuesCount: 1,
+ maxIssueCount: 0,
+ __typename: 'BoardList',
};
export const mockLabelList = {
@@ -449,11 +467,15 @@ export const mockLabelList = {
color: '#F0AD4E',
textColor: '#FFFFFF',
description: null,
+ descriptionHtml: null,
},
assignee: null,
milestone: null,
+ iteration: null,
loading: false,
issuesCount: 0,
+ maxIssueCount: 0,
+ __typename: 'BoardList',
};
export const mockMilestoneList = {
@@ -725,7 +747,7 @@ export const mockConfidentialToken = {
title: 'Confidential',
unique: true,
token: GlFilteredSearchToken,
- operators: [{ value: '=', description: 'is' }],
+ operators: OPERATOR_IS_ONLY,
options: [
{ icon: 'eye-slash', value: 'yes', title: 'Yes' },
{ icon: 'eye', value: 'no', title: 'No' },
@@ -735,12 +757,9 @@ export const mockConfidentialToken = {
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedIn) => [
{
icon: 'user',
- title: __('Assignee'),
- type: 'assignee',
- operators: [
- { value: '=', description: 'is' },
- { value: '!=', description: 'is not' },
- ],
+ title: TOKEN_TITLE_ASSIGNEE,
+ type: TOKEN_TYPE_ASSIGNEE,
+ operators: OPERATOR_IS_AND_IS_NOT,
token: AuthorToken,
unique: true,
fetchAuthors,
@@ -748,12 +767,9 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
},
{
icon: 'pencil',
- title: __('Author'),
- type: 'author',
- operators: [
- { value: '=', description: 'is' },
- { value: '!=', description: 'is not' },
- ],
+ title: TOKEN_TITLE_AUTHOR,
+ type: TOKEN_TYPE_AUTHOR,
+ operators: OPERATOR_IS_AND_IS_NOT,
symbol: '@',
token: AuthorToken,
unique: true,
@@ -762,12 +778,9 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
},
{
icon: 'labels',
- title: __('Label'),
- type: 'label',
- operators: [
- { value: '=', description: 'is' },
- { value: '!=', description: 'is not' },
- ],
+ title: TOKEN_TITLE_LABEL,
+ type: TOKEN_TYPE_LABEL,
+ operators: OPERATOR_IS_AND_IS_NOT,
token: LabelToken,
unique: false,
symbol: '~',
@@ -776,9 +789,9 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
...(isSignedIn ? [mockEmojiToken, mockConfidentialToken] : []),
{
icon: 'clock',
- title: __('Milestone'),
+ title: TOKEN_TITLE_MILESTONE,
symbol: '%',
- type: 'milestone',
+ type: TOKEN_TYPE_MILESTONE,
shouldSkipSort: true,
token: MilestoneToken,
unique: true,
@@ -786,8 +799,8 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
},
{
icon: 'issues',
- title: __('Type'),
- type: 'type',
+ title: TOKEN_TITLE_TYPE,
+ type: TOKEN_TYPE_TYPE,
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -796,8 +809,8 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
],
},
{
- type: 'release',
- title: __('Release'),
+ type: TOKEN_TYPE_RELEASE,
+ title: TOKEN_TITLE_RELEASE,
icon: 'rocket',
token: ReleaseToken,
fetchReleases: expect.any(Function),
@@ -844,6 +857,22 @@ export const mockGroupLabelsResponse = {
},
};
+export const boardListsQueryResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/1',
+ board: {
+ id: 'gid://gitlab/Board/1',
+ hideBacklogList: false,
+ lists: {
+ nodes: mockLists,
+ },
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
export const boardListQueryResponse = (issuesCount = 20) => ({
data: {
boardList: {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 78859525a63..b3e90e34161 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -8,7 +8,6 @@ import {
ListType,
issuableTypes,
BoardType,
- listsQuery,
DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
@@ -21,7 +20,7 @@ import {
getMoveData,
updateListPosition,
} from 'ee_else_ce/boards/boards_util';
-import { gqlClient } from '~/boards/graphql';
+import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import actions from '~/boards/stores/actions';
@@ -318,21 +317,18 @@ describe('fetchLists', () => {
};
const variables = {
- query: listsQuery[issuableType].query,
- variables: {
- fullPath: 'gitlab-org',
- boardId: fullBoardId,
- filters: {},
- isGroup,
- isProject,
- },
+ fullPath: 'gitlab-org',
+ boardId: fullBoardId,
+ filters: {},
+ isGroup,
+ isProject,
};
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
await actions.fetchLists({ commit, state, dispatch });
- expect(gqlClient.query).toHaveBeenCalledWith(variables);
+ expect(gqlClient.query).toHaveBeenCalledWith(expect.objectContaining({ variables }));
},
);
});
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
new file mode 100644
index 00000000000..6aab3b51806
--- /dev/null
+++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
@@ -0,0 +1,139 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Delete merged branches component Delete merged branches confirmation modal matches snapshot 1`] = `
+<div>
+ <b-button-stub
+ class="gl-mr-3 gl-button btn-danger-secondary"
+ data-qa-selector="delete_merged_branches_button"
+ size="md"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Delete merged branches
+
+ </span>
+ </b-button-stub>
+
+ <div>
+ <form
+ action="/namespace/project/-/merged_branches"
+ method="post"
+ >
+ <p>
+ You are about to
+ <strong>
+ delete all branches
+ </strong>
+ that were merged into
+ <code>
+ master
+ </code>
+ .
+ </p>
+
+ <p>
+
+ This may include merged branches that are not visible on the current screen.
+
+ </p>
+
+ <p>
+
+ A branch won't be deleted if it is protected or associated with an open merge request.
+
+ </p>
+
+ <p>
+ This bulk action is
+ <strong>
+ permanent and cannot be undone or recovered
+ </strong>
+ .
+ </p>
+
+ <p>
+ Plese type the following to confirm:
+ <code>
+ delete
+ </code>
+ .
+ <b-form-input-stub
+ aria-labelledby="input-label"
+ autocomplete="off"
+ class="gl-form-input gl-mt-2 gl-form-input-sm"
+ data-qa-selector="delete_merged_branches_input"
+ debounce="0"
+ formatter="[Function]"
+ type="text"
+ value=""
+ />
+ </p>
+
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="mock-csrf-token"
+ />
+ </form>
+ <div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3"
+ >
+ <b-button-stub
+ class="gl-button"
+ data-testid="delete-merged-branches-cancel-button"
+ size="md"
+ tag="button"
+ type="button"
+ variant="default"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Cancel
+
+ </span>
+ </b-button-stub>
+
+ <b-button-stub
+ class="gl-button"
+ data-qa-selector="delete_merged_branches_confirmation_button"
+ data-testid="delete-merged-branches-confirmation-button"
+ disabled="true"
+ size="md"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Delete merged branches
+ </span>
+ </b-button-stub>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
new file mode 100644
index 00000000000..4f1e772f4a4
--- /dev/null
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -0,0 +1,143 @@
+import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import DeleteMergedBranches, { i18n } from '~/branches/components/delete_merged_branches.vue';
+import { formPath, propsDataMock } from '../mock_data';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+let wrapper;
+
+const stubsData = {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+};
+
+const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
+ wrapper = mountFn(DeleteMergedBranches, {
+ propsData: {
+ ...propsDataMock,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs,
+ });
+};
+
+const findDeleteButton = () => wrapper.findComponent(GlButton);
+const findModal = () => wrapper.findComponent(GlModal);
+const findConfirmationButton = () =>
+ wrapper.findByTestId('delete-merged-branches-confirmation-button');
+const findCancelButton = () => wrapper.findByTestId('delete-merged-branches-cancel-button');
+const findFormInput = () => wrapper.findComponent(GlFormInput);
+const findForm = () => wrapper.find('form');
+const submitFormSpy = () => jest.spyOn(wrapper.vm.$refs.form, 'submit');
+
+describe('Delete merged branches component', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Delete merged branches button', () => {
+ it('has correct attributes, text and tooltip', () => {
+ expect(findDeleteButton().attributes()).toMatchObject({
+ category: 'secondary',
+ variant: 'danger',
+ });
+
+ expect(findDeleteButton().text()).toBe(i18n.deleteButtonText);
+ });
+
+ it('displays a tooltip', () => {
+ const tooltip = getBinding(findDeleteButton().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(wrapper.vm.buttonTooltipText);
+ });
+
+ it('opens modal when clicked', () => {
+ createComponent(mount);
+ jest.spyOn(wrapper.vm.$refs.modal, 'show');
+ findDeleteButton().trigger('click');
+
+ expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ });
+ });
+
+ describe('Delete merged branches confirmation modal', () => {
+ beforeEach(() => {
+ createComponent(shallowMountExtended, stubsData);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correct modal title and text', () => {
+ const modalText = findModal().text();
+ expect(findModal().props('title')).toBe(i18n.modalTitle);
+ expect(modalText).toContain(i18n.notVisibleBranchesWarning);
+ expect(modalText).toContain(i18n.protectedBranchWarning);
+ });
+
+ it('renders confirm and cancel buttons with correct text', () => {
+ expect(findConfirmationButton().text()).toContain(i18n.deleteButtonText);
+ expect(findCancelButton().text()).toContain(i18n.cancelButtonText);
+ });
+
+ it('renders form with correct attributes and hiden inputs', () => {
+ const form = findForm();
+ expect(form.attributes()).toEqual({
+ action: formPath,
+ method: 'post',
+ });
+ expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
+ expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
+ 'mock-csrf-token',
+ );
+ });
+
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a disabled confirm button by default', () => {
+ expect(findConfirmationButton().props('disabled')).toBe(true);
+ });
+
+ it('keeps disabled state when wrong input is provided', async () => {
+ findFormInput().vm.$emit('input', 'hello');
+ await waitForPromises();
+ expect(findConfirmationButton().props('disabled')).toBe(true);
+ findConfirmationButton().trigger('click');
+
+ expect(submitFormSpy()).not.toHaveBeenCalled();
+ findFormInput().trigger('keyup.enter');
+
+ expect(submitFormSpy()).not.toHaveBeenCalled();
+ });
+
+ it('submits form when correct amount is provided and the confirm button is clicked', async () => {
+ findFormInput().vm.$emit('input', 'delete');
+ await waitForPromises();
+ expect(findDeleteButton().props('disabled')).not.toBe(true);
+ findConfirmationButton().trigger('click');
+ expect(submitFormSpy()).toHaveBeenCalled();
+ });
+
+ it('calls hide on the modal when cancel button is clicked', () => {
+ const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
+ findCancelButton().trigger('click');
+ expect(closeModalSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/branches/mock_data.js b/spec/frontend/branches/mock_data.js
new file mode 100644
index 00000000000..9e8839d8ce9
--- /dev/null
+++ b/spec/frontend/branches/mock_data.js
@@ -0,0 +1,7 @@
+export const formPath = '/namespace/project/-/merged_branches';
+const defaultBranch = 'master';
+
+export const propsDataMock = {
+ formPath,
+ defaultBranch,
+};
diff --git a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
new file mode 100644
index 00000000000..ba948f12b33
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
@@ -0,0 +1,38 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import DeletePipelineScheduleModal from '~/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue';
+
+describe('Delete pipeline schedule modal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DeletePipelineScheduleModal, {
+ propsData: {
+ visible: true,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('emits the deleteSchedule event', async () => {
+ findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted()).toEqual({ deleteSchedule: [[]] });
+ });
+
+ it('emits the hideModal event', async () => {
+ findModal().vm.$emit('hide');
+
+ expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index 4b5a9611251..e5d9b378a42 100644
--- a/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlForm } from '@gitlab/ui';
-import PipelineSchedulesForm from '~/pipeline_schedules/components/pipeline_schedules_form.vue';
+import PipelineSchedulesForm from '~/ci/pipeline_schedules/components/pipeline_schedules_form.vue';
describe('Pipeline schedules form', () => {
let wrapper;
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
new file mode 100644
index 00000000000..4aa4cdf89a1
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -0,0 +1,280 @@
+import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { trimText } from 'helpers/text_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import PipelineSchedules from '~/ci/pipeline_schedules/components/pipeline_schedules.vue';
+import DeletePipelineScheduleModal from '~/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue';
+import TakeOwnershipModal from '~/ci/pipeline_schedules/components/take_ownership_modal.vue';
+import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
+import deletePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql';
+import takeOwnershipMutation from '~/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql';
+import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
+import {
+ mockGetPipelineSchedulesGraphQLResponse,
+ mockPipelineScheduleNodes,
+ deleteMutationResponse,
+ takeOwnershipMutationResponse,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+const $toast = {
+ show: jest.fn(),
+};
+
+describe('Pipeline schedules app', () => {
+ let wrapper;
+
+ const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
+ const deleteMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const takeOwnershipMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(takeOwnershipMutationResponse);
+ const takeOwnershipMutationHandlerFailed = jest
+ .fn()
+ .mockRejectedValue(new Error('GraphQL error'));
+
+ const createMockApolloProvider = (
+ requestHandlers = [[getPipelineSchedulesQuery, successHandler]],
+ ) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers) => {
+ wrapper = mountExtended(PipelineSchedules, {
+ provide: {
+ fullPath: 'gitlab-org/gitlab',
+ },
+ mocks: {
+ $toast,
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ });
+ };
+
+ const findTable = () => wrapper.findComponent(PipelineSchedulesTable);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findDeleteModal = () => wrapper.findComponent(DeletePipelineScheduleModal);
+ const findTakeOwnershipModal = () => wrapper.findComponent(TakeOwnershipModal);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findNewButton = () => wrapper.findByTestId('new-schedule-button');
+ const findAllTab = () => wrapper.findByTestId('pipeline-schedules-all-tab');
+ const findActiveTab = () => wrapper.findByTestId('pipeline-schedules-active-tab');
+ const findInactiveTab = () => wrapper.findByTestId('pipeline-schedules-inactive-tab');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays table, tabs and new button', async () => {
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ expect(findNewButton().exists()).toBe(true);
+ expect(findTabs().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('handles loading state', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('fetching pipeline schedules', () => {
+ it('fetches query and passes an array of pipeline schedules', async () => {
+ createComponent();
+
+ expect(successHandler).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
+ });
+
+ it('shows query error alert', async () => {
+ createComponent([[getPipelineSchedulesQuery, failedHandler]]);
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem fetching pipeline schedules.');
+ });
+ });
+
+ describe('deleting a pipeline schedule', () => {
+ it('shows delete mutation error alert', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteModal().vm.$emit('deleteSchedule');
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem deleting the pipeline schedule.');
+ });
+
+ it('deletes pipeline schedule and refetches query', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
+
+ await waitForPromises();
+
+ const scheduleId = mockPipelineScheduleNodes[0].id;
+
+ findTable().vm.$emit('showDeleteModal', scheduleId);
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+
+ findDeleteModal().vm.$emit('deleteSchedule');
+
+ await waitForPromises();
+
+ expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: scheduleId,
+ });
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect($toast.show).toHaveBeenCalledWith('Pipeline schedule successfully deleted.');
+ });
+
+ it('handles delete modal visibility correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDeleteModal().props('visible')).toBe(false);
+
+ findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findDeleteModal().props('visible')).toBe(true);
+ expect(findTakeOwnershipModal().props('visible')).toBe(false);
+
+ findDeleteModal().vm.$emit('hideModal');
+
+ await nextTick();
+
+ expect(findDeleteModal().props('visible')).toBe(false);
+ });
+ });
+
+ describe('taking ownership of a pipeline schedule', () => {
+ it('shows take ownership mutation error alert', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [takeOwnershipMutation, takeOwnershipMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findTakeOwnershipModal().vm.$emit('takeOwnership');
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(
+ 'There was a problem taking ownership of the pipeline schedule.',
+ );
+ });
+
+ it('takes ownership of pipeline schedule and refetches query', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [takeOwnershipMutation, takeOwnershipMutationHandlerSuccess],
+ ]);
+
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
+
+ await waitForPromises();
+
+ const scheduleId = mockPipelineScheduleNodes[1].id;
+
+ findTable().vm.$emit('showTakeOwnershipModal', scheduleId);
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+
+ findTakeOwnershipModal().vm.$emit('takeOwnership');
+
+ await waitForPromises();
+
+ expect(takeOwnershipMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: scheduleId,
+ });
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect($toast.show).toHaveBeenCalledWith('Successfully taken ownership from Admin.');
+ });
+
+ it('handles take ownership modal visibility correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTakeOwnershipModal().props('visible')).toBe(false);
+
+ findTable().vm.$emit('showTakeOwnershipModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findTakeOwnershipModal().props('visible')).toBe(true);
+ expect(findDeleteModal().props('visible')).toBe(false);
+
+ findTakeOwnershipModal().vm.$emit('hideModal');
+
+ await nextTick();
+
+ expect(findTakeOwnershipModal().props('visible')).toBe(false);
+ });
+ });
+
+ describe('pipeline schedule tabs', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('displays All tab with count', () => {
+ expect(trimText(findAllTab().text())).toBe(`All ${mockPipelineScheduleNodes.length}`);
+ });
+
+ it('displays Active tab with no count', () => {
+ expect(findActiveTab().text()).toBe('Active');
+ });
+
+ it('displays Inactive tab with no count', () => {
+ expect(findInactiveTab().text()).toBe('Inactive');
+ });
+
+ it('should refetch the schedules query on a tab click', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(0);
+
+ await findAllTab().trigger('click');
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index ecc1bdeb679..3364c61d155 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -1,7 +1,11 @@
import { GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineScheduleActions from '~/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
-import { mockPipelineScheduleNodes, mockPipelineScheduleAsGuestNodes } from '../../../mock_data';
+import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
+import {
+ mockPipelineScheduleNodes,
+ mockPipelineScheduleAsGuestNodes,
+ mockTakeOwnershipNodes,
+} from '../../../mock_data';
describe('Pipeline schedule actions', () => {
let wrapper;
@@ -20,6 +24,7 @@ describe('Pipeline schedule actions', () => {
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn');
+ const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
afterEach(() => {
wrapper.destroy();
@@ -46,4 +51,14 @@ describe('Pipeline schedule actions', () => {
showDeleteModal: [[mockPipelineScheduleNodes[0].id]],
});
});
+
+ it('take ownership button emits showTakeOwnershipModal event and schedule id', () => {
+ createComponent({ schedule: mockTakeOwnershipNodes[0] });
+
+ findTakeOwnershipBtn().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({
+ showTakeOwnershipModal: [[mockTakeOwnershipNodes[0].id]],
+ });
+ });
});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index 5a47b24232f..17bf465baf3 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -1,6 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
-import PipelineScheduleLastPipeline from '~/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
+import PipelineScheduleLastPipeline from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
describe('Pipeline schedule last pipeline', () => {
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
index b1bdc1e91a0..1c06c411097 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineScheduleNextRun from '~/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue';
+import PipelineScheduleNextRun from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
index 3ab04958f5e..6c1991cb4ac 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
@@ -1,6 +1,6 @@
import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineScheduleOwner from '~/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue';
+import PipelineScheduleOwner from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
describe('Pipeline schedule owner', () => {
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
index 6817e58790b..f531f04a736 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
@@ -1,6 +1,6 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineScheduleTarget from '~/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
+import PipelineScheduleTarget from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
describe('Pipeline schedule target', () => {
diff --git a/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
index 914897946ee..316b3bcf926 100644
--- a/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
@@ -1,6 +1,6 @@
import { GlTableLite } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue';
+import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
import { mockPipelineScheduleNodes } from '../../mock_data';
describe('Pipeline schedules table', () => {
diff --git a/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
index d787611fe8f..7e6d4ec4bf8 100644
--- a/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
@@ -1,13 +1,13 @@
import { GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue';
+import TakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
describe('Take ownership modal', () => {
let wrapper;
const url = `/root/job-log-tester/-/pipeline_schedules/3/take_ownership`;
const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(TakeOwnershipModal, {
+ wrapper = shallowMountExtended(TakeOwnershipModalLegacy, {
propsData: {
ownershipUrl: url,
...props,
@@ -21,10 +21,6 @@ describe('Take ownership modal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a primary action set to a url and a post data-method', () => {
const actionPrimary = findModal().props('actionPrimary');
@@ -45,10 +41,4 @@ describe('Take ownership modal', () => {
'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
);
});
-
- it('emits the cancel event when clicking on cancel', async () => {
- findModal().vm.$emit('cancel');
-
- expect(findModal().emitted('cancel')).toHaveLength(1);
- });
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
new file mode 100644
index 00000000000..e3965d13c19
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import TakeOwnershipModal from '~/ci/pipeline_schedules/components/take_ownership_modal.vue';
+
+describe('Take ownership modal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(TakeOwnershipModal, {
+ propsData: {
+ visible: true,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a take ownership message', () => {
+ expect(findModal().text()).toBe(
+ 'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
+ );
+ });
+
+ it('emits the takeOwnership event', async () => {
+ findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted()).toEqual({ takeOwnership: [[]] });
+ });
+
+ it('emits the hideModal event', async () => {
+ findModal().vm.$emit('hide');
+
+ expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 0a60998d8fb..3010f1d06c3 100644
--- a/spec/frontend/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -1,6 +1,7 @@
// Fixture located at spec/frontend/fixtures/pipeline_schedules.rb
import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
+import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json';
const {
data: {
@@ -18,10 +19,20 @@ const {
},
} = mockGetPipelineSchedulesAsGuestGraphQLResponse;
+const {
+ data: {
+ project: {
+ pipelineSchedules: { nodes: takeOwnershipNodes },
+ },
+ },
+} = mockGetPipelineSchedulesTakeOwnershipGraphQLResponse;
+
export const mockPipelineScheduleNodes = nodes;
export const mockPipelineScheduleAsGuestNodes = guestNodes;
+export const mockTakeOwnershipNodes = takeOwnershipNodes;
+
export const deleteMutationResponse = {
data: {
pipelineScheduleDelete: {
@@ -32,4 +43,20 @@ export const deleteMutationResponse = {
},
};
+export const takeOwnershipMutationResponse = {
+ data: {
+ pipelineScheduleTakeOwnership: {
+ pipelineSchedule: {
+ id: '1',
+ owner: {
+ id: '2',
+ name: 'Admin',
+ },
+ },
+ errors: [],
+ __typename: 'PipelineScheduleTakeOwnershipPayload',
+ },
+ },
+};
+
export { mockGetPipelineSchedulesGraphQLResponse };
diff --git a/spec/frontend/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 64f66d8f3ba..7081bc57467 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -8,23 +8,23 @@ import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerDetails from '~/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
-import RunnersJobs from '~/runner/components/runner_jobs.vue';
-
-import runnerQuery from '~/runner/graphql/show/runner.query.graphql';
-import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
+
+import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
+import AdminRunnerShowApp from '~/ci/runner/admin_runner_show/admin_runner_show_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerData } from '../mock_data';
-jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerData.data.runner;
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 7afde3bdc96..9778a6fe66c 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -14,18 +14,17 @@ import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
-import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
-import { createLocalState } from '~/runner/graphql/list/local_state';
-import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
-import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
-import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
-import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
-import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import { upgradeStatusTokenConfig } from 'ee_else_ce/ci/runner/components/search_tokens/upgrade_status_token_config';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+import AdminRunnersApp from '~/ci/runner/admin_runners/admin_runners_app.vue';
+import RunnerTypeTabs from '~/ci/runner/components/runner_type_tabs.vue';
+import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/ci/runner/components/runner_list.vue';
+import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
+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 {
ADMIN_FILTERED_SEARCH_NAMESPACE,
@@ -45,10 +44,10 @@ import {
STATUS_ONLINE,
DEFAULT_MEMBERSHIP,
RUNNER_PAGE_SIZE,
-} from '~/runner/constants';
-import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql';
-import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
-import { captureException } from '~/runner/sentry_utils';
+} from '~/ci/runner/constants';
+import allRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
import {
allRunnersData,
@@ -69,7 +68,7 @@ const mockRunnersHandler = jest.fn();
const mockRunnersCountHandler = jest.fn();
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
@@ -84,7 +83,6 @@ describe('AdminRunnersApp', () => {
let wrapper;
let showToast;
- const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
@@ -142,11 +140,6 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
- it('shows the feedback banner', () => {
- createComponent();
- expect(findRunnerStackedLayoutBanner().exists()).toBe(true);
- });
-
it('shows the runner setup instructions', () => {
createComponent();
diff --git a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap b/spec/frontend/ci/runner/components/__snapshots__/runner_status_popover_spec.js.snap
index b27a1adf01b..b27a1adf01b 100644
--- a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
+++ b/spec/frontend/ci/runner/components/__snapshots__/runner_status_popover_spec.js.snap
diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/ci/runner/components/cells/link_cell_spec.js
index 46ab1adb6b6..61bb4432c8e 100644
--- a/spec/frontend/runner/components/cells/link_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/link_cell_spec.js
@@ -1,6 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import LinkCell from '~/runner/components/cells/link_cell.vue';
+import LinkCell from '~/ci/runner/components/cells/link_cell.vue';
describe('LinkCell', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
index 58974d4f85f..82e262d1b73 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
@@ -1,9 +1,9 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
+import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import { allRunnersData } from '../../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
diff --git a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
index e9965d8855d..3097e43e583 100644
--- a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
@@ -3,9 +3,9 @@ import { GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import RunnerOwnerCell from '~/runner/components/cells/runner_owner_cell.vue';
+import RunnerOwnerCell from '~/ci/runner/components/cells/runner_owner_cell.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
describe('RunnerOwnerCell', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js
index e7cadefc140..4aa354f9b62 100644
--- a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js
@@ -1,12 +1,12 @@
import { __ } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerStackedSummaryCell from '~/runner/components/cells/runner_stacked_summary_cell.vue';
+import RunnerStackedSummaryCell from '~/ci/runner/components/cells/runner_stacked_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import RunnerTags from '~/runner/components/runner_tags.vue';
-import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue';
+import RunnerTags from '~/ci/runner/components/runner_tags.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 '~/runner/constants';
+import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import { allRunnersData } from '../../mock_data';
diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 1d4e3762c91..2fb824a8fa5 100644
--- a/spec/frontend/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
-import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
+import RunnerStatusCell from '~/ci/runner/components/cells/runner_status_cell.vue';
-import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
-import RunnerPausedBadge from '~/runner/components/runner_paused_badge.vue';
+import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
+import RunnerPausedBadge from '~/ci/runner/components/runner_paused_badge.vue';
import {
I18N_PAUSED,
I18N_STATUS_ONLINE,
@@ -10,7 +10,7 @@ import {
INSTANCE_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerStatusCell', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
index b49addf112f..f536e0dcbcf 100644
--- a/spec/frontend/runner/components/cells/runner_summary_field_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
@@ -1,6 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue';
+import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerSummaryField', () => {
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index d3f38bc1d26..cb46c668930 100644
--- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -7,11 +7,11 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
-import RegistrationToken from '~/runner/components/registration/registration_token.vue';
-import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
+import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
+import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
+import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 2510aaf0334..783a4d9252a 100644
--- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -6,14 +6,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-import runnersRegistrationTokenResetMutation from '~/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
-import { captureException } from '~/runner/sentry_utils';
+import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
Vue.use(GlToast);
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index 19344a68f79..d2a51c0d910 100644
--- a/spec/frontend/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RegistrationToken from '~/runner/components/registration/registration_token.vue';
+import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
const mockToken = '01234567890';
diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
index cc09046c000..5df2e04c340 100644
--- a/spec/frontend/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
@@ -1,7 +1,7 @@
import { GlAvatar, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+import RunnerAssignedItem from '~/ci/runner/components/runner_assigned_item.vue';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
const mockHref = '/group/project';
diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_checkbox_spec.js
index 424a4e61ccd..dad36b0179f 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_checkbox_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import { GlFormCheckbox } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
+import RunnerBulkDeleteCheckbox from '~/ci/runner/components/runner_bulk_delete_checkbox.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { createLocalState } from '~/runner/graphql/list/local_state';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
Vue.use(VueApollo);
diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 6df918c684f..64f5a0e3b57 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -5,10 +5,10 @@ import { createAlert } from '~/flash';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
-import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
+import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
-import BulkRunnerDeleteMutation from '~/runner/graphql/list/bulk_runner_delete.mutation.graphql';
-import { createLocalState } from '~/runner/graphql/list/local_state';
+import BulkRunnerDeleteMutation from '~/ci/runner/graphql/list/bulk_runner_delete.mutation.graphql';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
import waitForPromises from 'helpers/wait_for_promises';
import { allRunnersData } from '../mock_data';
@@ -72,7 +72,6 @@ describe('RunnerBulkDelete', () => {
afterEach(() => {
bulkRunnerDeleteHandler.mockReset();
- wrapper.destroy();
});
describe('When no runners are checked', () => {
@@ -126,50 +125,61 @@ describe('RunnerBulkDelete', () => {
let evt;
let mockHideModal;
+ const confirmDeletion = () => {
+ evt = {
+ preventDefault: jest.fn(),
+ };
+ findModal().vm.$emit('primary', evt);
+ };
+
beforeEach(() => {
mockCheckedRunnerIds = [mockId1, mockId2];
createComponent();
jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
- mockHideModal = jest.spyOn(findModal().vm, 'hide');
+ mockHideModal = jest.spyOn(findModal().vm, 'hide').mockImplementation(() => {});
});
- describe('when deletion is successful', () => {
+ describe('when deletion is confirmed', () => {
beforeEach(() => {
- bulkRunnerDeleteHandler.mockResolvedValue({
- data: {
- bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
- },
- });
-
- evt = {
- preventDefault: jest.fn(),
- };
- findModal().vm.$emit('primary', evt);
+ confirmDeletion();
});
- it('has loading state', async () => {
+ it('has loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
expect(findModal().props('actionCancel').attributes.loading).toBe(true);
-
- await waitForPromises();
-
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
- expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
it('modal is not prevented from closing', () => {
expect(evt.preventDefault).toHaveBeenCalledTimes(1);
});
- it('mutation is called', async () => {
+ it('mutation is called', () => {
expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
input: { ids: mockCheckedRunnerIds },
});
});
+ });
- it('user interface is updated', async () => {
+ describe('when deletion is successful', () => {
+ beforeEach(async () => {
+ bulkRunnerDeleteHandler.mockResolvedValue({
+ data: {
+ bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
+ },
+ });
+
+ confirmDeletion();
+ await waitForPromises();
+ });
+
+ it('removes loading state', () => {
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(false);
+ });
+
+ it('user interface is updated', () => {
const { evict, gc } = apolloCache;
expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length);
@@ -183,44 +193,80 @@ describe('RunnerBulkDelete', () => {
expect(gc).toHaveBeenCalledTimes(1);
});
+ it('emits deletion confirmation', () => {
+ expect(wrapper.emitted('deleted')).toEqual([
+ [{ message: expect.stringContaining(`${mockCheckedRunnerIds.length}`) }],
+ ]);
+ });
+
it('modal is hidden', () => {
expect(mockHideModal).toHaveBeenCalledTimes(1);
});
});
- describe('when deletion fails', () => {
- beforeEach(() => {
- bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!'));
-
- evt = {
- preventDefault: jest.fn(),
- };
- findModal().vm.$emit('primary', evt);
- });
-
- it('has loading state', async () => {
- expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
- expect(findModal().props('actionCancel').attributes.loading).toBe(true);
+ describe('when deletion fails partially', () => {
+ beforeEach(async () => {
+ bulkRunnerDeleteHandler.mockResolvedValue({
+ data: {
+ bulkRunnerDelete: {
+ deletedIds: [mockId1], // only one runner could be deleted
+ errors: ['Can only delete up to 1 runners per call. Ignored 1 runner(s).'],
+ },
+ },
+ });
+ confirmDeletion();
await waitForPromises();
+ });
+ it('removes loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
- it('modal is not prevented from closing', () => {
- expect(evt.preventDefault).toHaveBeenCalledTimes(1);
+ it('user interface is partially updated', () => {
+ const { evict, gc } = apolloCache;
+
+ expect(evict).toHaveBeenCalledTimes(1);
+ expect(evict).toHaveBeenCalledWith({
+ id: expect.stringContaining(mockId1),
+ });
+
+ expect(gc).toHaveBeenCalledTimes(1);
});
- it('mutation is called', () => {
- expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
- input: { ids: mockCheckedRunnerIds },
+ it('emits deletion confirmation', () => {
+ expect(wrapper.emitted('deleted')).toEqual([[{ message: expect.stringContaining('1') }]]);
+ });
+
+ it('alert is called', () => {
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.any(String),
+ captureError: true,
+ error: expect.any(Error),
});
});
- it('user interface is not updated', async () => {
+ it('modal is hidden', () => {
+ expect(mockHideModal).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when deletion fails', () => {
+ beforeEach(async () => {
+ bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!'));
+
+ confirmDeletion();
await waitForPromises();
+ });
+ it('resolves loading state', () => {
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(false);
+ });
+
+ it('user interface is not updated', () => {
const { evict, gc } = apolloCache;
expect(evict).not.toHaveBeenCalled();
@@ -228,9 +274,11 @@ describe('RunnerBulkDelete', () => {
expect(mockState.localMutations.clearChecked).not.toHaveBeenCalled();
});
- it('alert is called', async () => {
- await waitForPromises();
+ it('does not emit deletion confirmation', () => {
+ expect(wrapper.emitted('deleted')).toBeUndefined();
+ });
+ it('alert is called', () => {
expect(createAlert).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalledWith({
message: expect.any(String),
@@ -238,6 +286,10 @@ describe('RunnerBulkDelete', () => {
error: expect.any(Error),
});
});
+
+ it('modal is hidden', () => {
+ expect(mockHideModal).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
index c8fb7a69379..02960ad427e 100644
--- a/spec/frontend/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -4,24 +4,25 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerDeleteMutation from '~/runner/graphql/shared/runner_delete.mutation.graphql';
+import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
-import { I18N_DELETE_RUNNER } from '~/runner/constants';
+import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
-import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
import { allRunnersData } from '../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
+const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
Vue.use(VueApollo);
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
let wrapper;
@@ -96,7 +97,7 @@ describe('RunnerDeleteButton', () => {
});
it('Displays a modal with the runner name', () => {
- expect(findModal().props('runnerName')).toBe(`#${mockRunnerId} (${mockRunner.shortSha})`);
+ expect(findModal().props('runnerName')).toBe(mockRunnerName);
});
it('Does not have tabindex when button is enabled', () => {
@@ -189,6 +190,10 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: mockErrorMsg,
+ });
});
});
@@ -217,6 +222,10 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: `${mockErrorMsg} ${mockErrorMsg2}`,
+ });
});
it('does not evict runner from apollo cache', () => {
diff --git a/spec/frontend/runner/components/runner_delete_modal_spec.js b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
index 3e5b634d815..f2fb0206763 100644
--- a/spec/frontend/runner/components/runner_delete_modal_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
@@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
+import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
describe('RunnerDeleteModal', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index e6cc936e260..65a81973869 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -3,13 +3,13 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { useFakeDate } from 'helpers/fake_date';
import { findDd } from 'helpers/dl_locator_helper';
-import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
+import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/ci/runner/constants';
-import RunnerDetails from '~/runner/components/runner_details.vue';
-import RunnerDetail from '~/runner/components/runner_detail.vue';
-import RunnerGroups from '~/runner/components/runner_groups.vue';
-import RunnerTags from '~/runner/components/runner_tags.vue';
-import RunnerTag from '~/runner/components/runner_tag.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerDetail from '~/ci/runner/components/runner_detail.vue';
+import RunnerGroups from '~/ci/runner/components/runner_groups.vue';
+import RunnerTags from '~/ci/runner/components/runner_tags.vue';
+import RunnerTag from '~/ci/runner/components/runner_tag.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_edit_button_spec.js b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
index 428c1ef07e9..907cdc90100 100644
--- a/spec/frontend/runner/components/runner_edit_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerEditButton', () => {
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index c92e19f9263..496c144083e 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,9 +1,9 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
-import TagToken from '~/runner/components/search_tokens/tag_token.vue';
-import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
+import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
+import { statusTokenConfig } from '~/ci/runner/components/search_tokens/status_token_config';
+import TagToken from '~/ci/runner/components/search_tokens/tag_token.vue';
+import { tagTokenConfig } from '~/ci/runner/components/search_tokens/tag_token_config';
import {
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -12,7 +12,7 @@ import {
DEFAULT_MEMBERSHIP,
DEFAULT_SORT,
CONTACTED_DESC,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
diff --git a/spec/frontend/runner/components/runner_groups_spec.js b/spec/frontend/ci/runner/components/runner_groups_spec.js
index b83733b9972..0991feb2e55 100644
--- a/spec/frontend/runner/components/runner_groups_spec.js
+++ b/spec/frontend/ci/runner/components/runner_groups_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerGroups from '~/runner/components/runner_groups.vue';
-import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+import RunnerGroups from '~/ci/runner/components/runner_groups.vue';
+import RunnerAssignedItem from '~/ci/runner/components/runner_assigned_item.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index 701d39108cb..a04011de1cd 100644
--- a/spec/frontend/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -1,13 +1,18 @@
import { GlSprintf } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { I18N_STATUS_ONLINE, I18N_GROUP_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
+import {
+ I18N_STATUS_ONLINE,
+ I18N_GROUP_TYPE,
+ GROUP_TYPE,
+ STATUS_ONLINE,
+} from '~/ci/runner/constants';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
-import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
+import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
import { runnerData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/ci/runner/components/runner_jobs_spec.js
index 4d38afb25ee..bdb8a4a31a3 100644
--- a/spec/frontend/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_spec.js
@@ -5,18 +5,18 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import RunnerJobs from '~/runner/components/runner_jobs.vue';
-import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants';
+import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
+import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants';
-import runnerJobsQuery from '~/runner/graphql/show/runner_jobs.query.graphql';
+import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql';
import { runnerData, runnerJobsData } from '../mock_data';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
const mockRunnerWithJobs = runnerJobsData.data.runner;
diff --git a/spec/frontend/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 5f4905ad2a8..8defe568df8 100644
--- a/spec/frontend/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -6,7 +6,7 @@ import {
} from 'helpers/vue_test_utils_helper';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
+import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
import { useFakeDate } from 'helpers/fake_date';
import { runnerJobsData } from '../mock_data';
@@ -61,6 +61,8 @@ describe('RunnerJobsTable', () => {
__('Project'),
__('Commit'),
s__('Job|Finished at'),
+ s__('Job|Duration'),
+ s__('Job|Queued'),
s__('Runners|Tags'),
]);
});
@@ -108,6 +110,22 @@ describe('RunnerJobsTable', () => {
expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago');
});
+ it('Formats duration time', () => {
+ mockJobsCopy[0].duration = 60;
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'duration' }).text()).toBe('00:01:00');
+ });
+
+ it('Formats queued time', () => {
+ mockJobsCopy[0].queuedDuration = 30;
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'queued' }).text()).toBe('00:00:30');
+ });
+
it('Formats tags', () => {
mockJobsCopy[0].tags = ['tag-1', 'tag-2'];
diff --git a/spec/frontend/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index 038162b889e..d351f7b6908 100644
--- a/spec/frontend/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
+import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
const mockSvgPath = 'mock-svg-path.svg';
const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index a31990f8f7e..d53a0ce8f4f 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -8,13 +8,13 @@ import {
import createMockApollo from 'helpers/mock_apollo_helper';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createLocalState } from '~/runner/graphql/list/local_state';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
-import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
-import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
+import RunnerList from '~/ci/runner/components/runner_list.vue';
+import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
+import RunnerBulkDeleteCheckbox from '~/ci/runner/components/runner_bulk_delete_checkbox.vue';
-import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants';
+import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/ci/runner/constants';
import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
const mockRunners = allRunnersData.data.runners.nodes;
diff --git a/spec/frontend/runner/components/runner_membership_toggle_spec.js b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
index 1a7ae22618a..f089becd400 100644
--- a/spec/frontend/runner/components/runner_membership_toggle_spec.js
+++ b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
@@ -1,11 +1,11 @@
import { GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue';
+import RunnerMembershipToggle from '~/ci/runner/components/runner_membership_toggle.vue';
import {
I18N_SHOW_ONLY_INHERITED,
MEMBERSHIP_DESCENDANTS,
MEMBERSHIP_ALL_AVAILABLE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerMembershipToggle', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_pagination_spec.js b/spec/frontend/ci/runner/components/runner_pagination_spec.js
index 499cc59250d..f835ee4514d 100644
--- a/spec/frontend/runner/components/runner_pagination_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pagination_spec.js
@@ -1,6 +1,6 @@
import { GlKeysetPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
const mockStartCursor = 'START_CURSOR';
const mockEndCursor = 'END_CURSOR';
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
index 61476007571..12680e01b98 100644
--- a/spec/frontend/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -4,18 +4,18 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerToggleActiveMutation from '~/runner/graphql/shared/runner_toggle_active.mutation.graphql';
+import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import { createAlert } from '~/flash';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
I18N_RESUME,
I18N_RESUME_TOOLTIP,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import { allRunnersData } from '../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
@@ -23,7 +23,7 @@ const mockRunner = allRunnersData.data.runners.nodes[0];
Vue.use(VueApollo);
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
describe('RunnerPauseButton', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_paused_badge_spec.js b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
index c1c7351aab2..b051ebe99a7 100644
--- a/spec/frontend/runner/components/runner_paused_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
@@ -1,8 +1,8 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue';
+import RunnerStatePausedBadge from '~/ci/runner/components/runner_paused_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { I18N_PAUSED } from '~/runner/constants';
+import { I18N_PAUSED } from '~/ci/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/ci/runner/components/runner_projects_spec.js
index eca042cae86..17517c4db66 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/ci/runner/components/runner_projects_spec.js
@@ -12,18 +12,18 @@ import {
I18N_FILTER_PROJECTS,
I18N_NO_PROJECTS_FOUND,
RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
-} from '~/runner/constants';
-import RunnerProjects from '~/runner/components/runner_projects.vue';
-import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
-import { captureException } from '~/runner/sentry_utils';
+} from '~/ci/runner/constants';
+import RunnerProjects from '~/ci/runner/components/runner_projects.vue';
+import RunnerAssignedItem from '~/ci/runner/components/runner_assigned_item.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
-import runnerProjectsQuery from '~/runner/graphql/show/runner_projects.query.graphql';
+import runnerProjectsQuery from '~/ci/runner/graphql/show/runner_projects.query.graphql';
import { runnerData, runnerProjectsData } from '../mock_data';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
const mockRunnerWithProjects = runnerProjectsData.data.runner;
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
index 9ab6378304f..7d3064c2aef 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
+import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
I18N_STATUS_ONLINE,
@@ -13,7 +13,7 @@ import {
STATUS_OFFLINE,
STATUS_STALE,
STATUS_NEVER_CONTACTED,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_status_popover_spec.js b/spec/frontend/ci/runner/components/runner_status_popover_spec.js
index 789283d1245..89fb95f2da4 100644
--- a/spec/frontend/runner/components/runner_status_popover_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_popover_spec.js
@@ -1,6 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import RunnerStatusPopover from '~/ci/runner/components/runner_status_popover.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/ci/runner/components/runner_tag_spec.js
index 391c17f81cb..7bcb046ae43 100644
--- a/spec/frontend/runner/components/runner_tag_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tag_spec.js
@@ -2,8 +2,8 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { RUNNER_TAG_BADGE_VARIANT } from '~/runner/constants';
-import RunnerTag from '~/runner/components/runner_tag.vue';
+import { RUNNER_TAG_BADGE_VARIANT } from '~/ci/runner/constants';
+import RunnerTag from '~/ci/runner/components/runner_tag.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
const mockTag = 'tag1';
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/ci/runner/components/runner_tags_spec.js
index c6bfabdb18a..96bec00302b 100644
--- a/spec/frontend/runner/components/runner_tags_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tags_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import RunnerTags from '~/runner/components/runner_tags.vue';
+import RunnerTags from '~/ci/runner/components/runner_tags.vue';
describe('RunnerTags', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
index fe922fb9d18..58f09362759 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
+import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
INSTANCE_TYPE,
@@ -9,7 +9,7 @@ import {
I18N_INSTANCE_TYPE,
I18N_GROUP_TYPE,
I18N_PROJECT_TYPE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
index dde35533bc3..3347c190083 100644
--- a/spec/frontend/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
@@ -1,14 +1,14 @@
import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import RunnerCount from '~/runner/components/stat/runner_count.vue';
+import RunnerTypeTabs from '~/ci/runner/components/runner_type_tabs.vue';
+import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
DEFAULT_MEMBERSHIP,
DEFAULT_SORT,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
const mockSearch = {
runnerType: null,
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index e12736216a0..a0e51ebf958 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -7,22 +7,22 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
-import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
+import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
ACCESS_LEVEL_REF_PROTECTED,
ACCESS_LEVEL_NOT_PROTECTED,
-} from '~/runner/constants';
-import runnerUpdateMutation from '~/runner/graphql/edit/runner_update.mutation.graphql';
-import { captureException } from '~/runner/sentry_utils';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+} from '~/ci/runner/constants';
+import runnerUpdateMutation from '~/ci/runner/graphql/edit/runner_update.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerFormData } from '../mock_data';
-jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerFormData.data.runner;
diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index a7363eb11cd..d3c7ea50f9d 100644
--- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -6,7 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue';
+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 { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
diff --git a/spec/frontend/runner/components/stat/runner_count_spec.js b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
index 2a6a745099f..42d8c9a1080 100644
--- a/spec/frontend/runner/components/stat/runner_count_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
@@ -1,18 +1,18 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
-import RunnerCount from '~/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
-import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
-import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
import { runnersCountData, groupRunnersCountData } from '../../mock_data';
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
diff --git a/spec/frontend/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
index 964a6a6ff71..cad61f26012 100644
--- a/spec/frontend/runner/components/stat/runner_single_stat_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
@@ -1,8 +1,8 @@
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
-import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
-import RunnerCount from '~/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+import RunnerSingleStat from '~/ci/runner/components/stat/runner_single_stat.vue';
+import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
describe('RunnerStats', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
index 4afbe453903..daebf3df050 100644
--- a/spec/frontend/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
@@ -1,6 +1,6 @@
import { shallowMount, mount } from '@vue/test-utils';
-import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
+import RunnerStats from '~/ci/runner/components/stat/runner_stats.vue';
+import RunnerSingleStat from '~/ci/runner/components/stat/runner_single_stat.vue';
import {
I18N_STATUS_ONLINE,
I18N_STATUS_OFFLINE,
@@ -9,7 +9,7 @@ import {
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerStats', () => {
let wrapper;
diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/ci/runner/graphql/local_state_spec.js
index 915170b53f9..ce07a6a618d 100644
--- a/spec/frontend/runner/graphql/local_state_spec.js
+++ b/spec/frontend/ci/runner/graphql/local_state_spec.js
@@ -1,8 +1,8 @@
import { gql } from '@apollo/client/core';
import createApolloClient from '~/lib/graphql';
-import { createLocalState } from '~/runner/graphql/list/local_state';
-import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
-import { RUNNER_TYPENAME } from '~/runner/constants';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+import getCheckedRunnerIdsQuery from '~/ci/runner/graphql/list/checked_runner_ids.query.graphql';
+import { RUNNER_TYPENAME } from '~/ci/runner/constants';
const makeRunner = (id, deleteRunner = true) => ({
id,
@@ -11,7 +11,7 @@ const makeRunner = (id, deleteRunner = true) => ({
},
});
-describe('~/runner/graphql/list/local_state', () => {
+describe('~/ci/runner/graphql/list/local_state', () => {
let localState;
let apolloClient;
diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index a3b67674c94..c6c3f3b7040 100644
--- a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -7,21 +7,21 @@ import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerDetails from '~/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
-import runnerQuery from '~/runner/graphql/show/runner.query.graphql';
-import GroupRunnerShowApp from '~/runner/group_runner_show/group_runner_show_app.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
+import GroupRunnerShowApp from '~/ci/runner/group_runner_show/group_runner_show_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerData } from '../mock_data';
-jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerData.data.runner;
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 7482926e151..c3493b3c9fd 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -13,19 +13,18 @@ import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
-import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
-import { createLocalState } from '~/runner/graphql/list/local_state';
-
-import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
-import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
-import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
-import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
-import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue';
+import { upgradeStatusTokenConfig } from 'ee_else_ce/ci/runner/components/search_tokens/upgrade_status_token_config';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+
+import RunnerTypeTabs from '~/ci/runner/components/runner_type_tabs.vue';
+import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/ci/runner/components/runner_list.vue';
+import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
+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 RunnerMembershipToggle from '~/ci/runner/components/runner_membership_toggle.vue';
import {
CREATED_ASC,
@@ -46,11 +45,11 @@ import {
MEMBERSHIP_DESCENDANTS,
RUNNER_PAGE_SIZE,
I18N_EDIT,
-} from '~/runner/constants';
-import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
-import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
-import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
-import { captureException } from '~/runner/sentry_utils';
+} from '~/ci/runner/constants';
+import groupRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
+import GroupRunnersApp from '~/ci/runner/group_runners/group_runners_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
import {
groupRunnersData,
groupRunnersDataPaginated,
@@ -74,7 +73,7 @@ const mockGroupRunnersHandler = jest.fn();
const mockGroupRunnersCountHandler = jest.fn();
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
@@ -83,7 +82,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => {
let wrapper;
- const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
@@ -142,11 +140,6 @@ describe('GroupRunnersApp', () => {
wrapper.destroy();
});
- it('shows the feedback banner', () => {
- createComponent();
- expect(findRunnerStackedLayoutBanner().exists()).toBe(true);
- });
-
it('shows the runner tabs with a runner count for each type', async () => {
await createComponent({ mountFn: mountExtended });
@@ -391,9 +384,9 @@ describe('GroupRunnersApp', () => {
expect(findRunnerPagination().attributes('disabled')).toBe('true');
});
- it('runners cannot be deleted in bulk', () => {
+ it('runners can be deleted in bulk', () => {
createComponent();
- expect(findRunnerList().props('checkable')).toBe(false);
+ expect(findRunnerList().props('checkable')).toBe(true);
});
describe('when no runners are found', () => {
diff --git a/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/save_alert_to_local_storage_spec.js
index 69cda6d6022..b34ef01eeed 100644
--- a/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js
+++ b/spec/frontend/ci/runner/local_storage_alert/save_alert_to_local_storage_spec.js
@@ -1,6 +1,6 @@
import AccessorUtilities from '~/lib/utils/accessor';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
-import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
const mockAlert = { message: 'Message!' };
diff --git a/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
index cabbe642dac..03908891cfd 100644
--- a/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js
+++ b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
@@ -1,6 +1,6 @@
import AccessorUtilities from '~/lib/utils/accessor';
-import { showAlertFromLocalStorage } from '~/runner/local_storage_alert/show_alert_from_local_storage';
-import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants';
+import { showAlertFromLocalStorage } from '~/ci/runner/local_storage_alert/show_alert_from_local_storage';
+import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { createAlert } from '~/flash';
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index da0c0433b3e..eff5abc21b5 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -1,23 +1,23 @@
// Fixtures generated by: spec/frontend/fixtures/runner.rb
// Show runner queries
-import runnerData from 'test_fixtures/graphql/runner/show/runner.query.graphql.json';
-import runnerWithGroupData from 'test_fixtures/graphql/runner/show/runner.query.graphql.with_group.json';
-import runnerProjectsData from 'test_fixtures/graphql/runner/show/runner_projects.query.graphql.json';
-import runnerJobsData from 'test_fixtures/graphql/runner/show/runner_jobs.query.graphql.json';
+import runnerData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.json';
+import runnerWithGroupData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.with_group.json';
+import runnerProjectsData from 'test_fixtures/graphql/ci/runner/show/runner_projects.query.graphql.json';
+import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.query.graphql.json';
// Edit runner queries
-import runnerFormData from 'test_fixtures/graphql/runner/edit/runner_form.query.graphql.json';
+import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json';
// List queries
-import allRunnersData from 'test_fixtures/graphql/runner/list/all_runners.query.graphql.json';
-import allRunnersDataPaginated from 'test_fixtures/graphql/runner/list/all_runners.query.graphql.paginated.json';
-import runnersCountData from 'test_fixtures/graphql/runner/list/all_runners_count.query.graphql.json';
-import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.json';
-import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.paginated.json';
-import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runners_count.query.graphql.json';
+import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
+import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
+import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
+import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
+import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
+import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
-import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/runner/constants';
+import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/ci/runner/constants';
const emptyPageInfo = {
__typename: 'PageInfo',
diff --git a/spec/frontend/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
index fb118817d51..a9369a5e626 100644
--- a/spec/frontend/runner/runner_edit/runner_edit_app_spec.js
+++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
@@ -6,17 +6,17 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
-import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql';
-import RunnerEditApp from '~//runner/runner_edit/runner_edit_app.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/runner/constants';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
+import runnerFormQuery from '~/ci/runner/graphql/edit/runner_form.query.graphql';
+import RunnerEditApp from '~/ci/runner/runner_edit/runner_edit_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/ci/runner/constants';
import { runnerFormData } from '../mock_data';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerFormData.data.runner;
const mockRunnerGraphqlId = mockRunner.id;
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/ci/runner/runner_search_utils_spec.js
index e1f90482b34..1db8fa1829b 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_search_utils_spec.js
@@ -5,7 +5,7 @@ import {
fromSearchToUrl,
fromSearchToVariables,
isSearchFiltered,
-} from 'ee_else_ce/runner/runner_search_utils';
+} from 'ee_else_ce/ci/runner/runner_search_utils';
import { mockSearchExamples } from './mock_data';
describe('search_params.js', () => {
diff --git a/spec/frontend/runner/runner_update_form_utils_spec.js b/spec/frontend/ci/runner/runner_update_form_utils_spec.js
index a633aee92f7..b2f7bbc49a9 100644
--- a/spec/frontend/runner/runner_update_form_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_update_form_utils_spec.js
@@ -1,5 +1,8 @@
-import { ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
-import { modelToUpdateMutationVariables, runnerToModel } from '~/runner/runner_update_form_utils';
+import { ACCESS_LEVEL_NOT_PROTECTED } from '~/ci/runner/constants';
+import {
+ modelToUpdateMutationVariables,
+ runnerToModel,
+} from '~/ci/runner/runner_update_form_utils';
const mockId = 'gid://gitlab/Ci::Runner/1';
const mockDescription = 'Runner Desc.';
@@ -20,7 +23,7 @@ const mockModel = {
tagList: 'tag-1, tag-2',
};
-describe('~/runner/runner_update_form_utils', () => {
+describe('~/ci/runner/runner_update_form_utils', () => {
describe('runnerToModel', () => {
it('collects all model data', () => {
expect(runnerToModel(mockRunner)).toEqual(mockModel);
diff --git a/spec/frontend/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index b61eb63961e..f7b689272ce 100644
--- a/spec/frontend/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/browser';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
jest.mock('@sentry/browser');
-describe('~/runner/sentry_utils', () => {
+describe('~/ci/runner/sentry_utils', () => {
let mockSetTag;
beforeEach(async () => {
diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/ci/runner/utils_spec.js
index 33de1345f85..56b758f00e4 100644
--- a/spec/frontend/runner/utils_spec.js
+++ b/spec/frontend/ci/runner/utils_spec.js
@@ -1,6 +1,11 @@
-import { formatJobCount, tableField, getPaginationVariables, parseInterval } from '~/runner/utils';
+import {
+ formatJobCount,
+ tableField,
+ getPaginationVariables,
+ parseInterval,
+} from '~/ci/runner/utils';
-describe('~/runner/utils', () => {
+describe('~/ci/runner/utils', () => {
describe('formatJobCount', () => {
it('formats a number', () => {
expect(formatJobCount(1)).toBe('1');
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 864041141b8..c7375acd8e5 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
@@ -1,178 +1,35 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/settings';
import ciAdminVariables from '~/ci_variable_list/components/ci_admin_variables.vue';
-import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
-import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
-import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
-import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
-
-import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
-
-import { mockAdminVariables, newVariable } from '../mocks';
-
-jest.mock('~/flash');
-
-Vue.use(VueApollo);
-
-const mockProvide = {
- endpoint: '/variables',
-};
-
-describe('Ci Admin Variable list', () => {
+describe('Ci Project Variable wrapper', () => {
let wrapper;
- let mockApollo;
- let mockVariables;
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiTable = () => wrapper.findComponent(GlTable);
- const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
-
- // eslint-disable-next-line consistent-return
- const createComponentWithApollo = async ({ isLoading = false } = {}) => {
- const handlers = [[getAdminVariables, mockVariables]];
-
- mockApollo = createMockApollo(handlers, resolvers);
+ const findCiShared = () => wrapper.findComponent(ciVariableShared);
- wrapper = shallowMount(ciAdminVariables, {
- provide: mockProvide,
- apolloProvider: mockApollo,
- stubs: { ciVariableSettings, ciVariableTable },
- });
-
- if (!isLoading) {
- return waitForPromises();
- }
+ const createComponent = () => {
+ wrapper = shallowMount(ciAdminVariables);
};
beforeEach(() => {
- mockVariables = jest.fn();
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
});
- describe('while queries are being fetch', () => {
- beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
- });
-
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
+ it('Passes down the correct props to ci_variable_shared', () => {
+ expect(findCiShared().props()).toEqual({
+ areScopedVariablesAvailable: false,
+ componentName: 'InstanceVariables',
+ hideEnvironmentScope: true,
+ mutationData: wrapper.vm.$options.mutationData,
+ queryData: wrapper.vm.$options.queryData,
+ refetchAfterMutation: true,
+ fullPath: null,
+ id: null,
});
});
-
- describe('when queries are resolved', () => {
- describe('successfuly', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockAdminVariables);
-
- await createComponentWithApollo();
- });
-
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([]);
- });
-
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockAdminVariables.data.ciVariables.nodes,
- );
- });
-
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
-
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockVariables.mockRejectedValue();
-
- await createComponentWithApollo();
- });
-
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
- });
- });
- });
-
- describe('mutations', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockAdminVariables);
-
- await createComponentWithApollo();
- });
- it.each`
- actionName | mutation | event
- ${'add'} | ${addAdminVariable} | ${'add-variable'}
- ${'update'} | ${updateAdminVariable} | ${'update-variable'}
- ${'delete'} | ${deleteAdminVariable} | ${'delete-variable'}
- `(
- 'calls the right mutation when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event | mutationName
- ${'add'} | ${'add-variable'} | ${'addAdminVariable'}
- ${'update'} | ${'update-variable'} | ${'updateAdminVariable'}
- ${'delete'} | ${'delete-variable'} | ${'deleteAdminVariable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event, mutationName }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
- });
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
- });
});
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 8a48e73eb9f..ef5a86ccb61 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
@@ -1,183 +1,72 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/settings';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue';
-import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
-import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
-import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
-import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
-
-import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
-
-import { mockGroupVariables, newVariable } from '../mocks';
-
-jest.mock('~/flash');
-
-Vue.use(VueApollo);
+import { GRAPHQL_GROUP_TYPE } from '~/ci_variable_list/constants';
const mockProvide = {
- endpoint: '/variables',
- groupPath: '/namespace/group',
- groupId: 1,
+ glFeatures: {
+ groupScopedCiVariables: false,
+ },
+ groupPath: '/group',
+ groupId: 12,
};
-describe('Ci Group Variable list', () => {
+describe('Ci Group Variable wrapper', () => {
let wrapper;
- let mockApollo;
- let mockVariables;
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiTable = () => wrapper.findComponent(GlTable);
- const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
-
- // eslint-disable-next-line consistent-return
- const createComponentWithApollo = async ({ isLoading = false } = {}) => {
- const handlers = [[getGroupVariables, mockVariables]];
-
- mockApollo = createMockApollo(handlers, resolvers);
+ const findCiShared = () => wrapper.findComponent(ciVariableShared);
+ const createComponent = ({ provide = {} } = {}) => {
wrapper = shallowMount(ciGroupVariables, {
- provide: mockProvide,
- apolloProvider: mockApollo,
- stubs: { ciVariableSettings, ciVariableTable },
+ provide: { ...mockProvide, ...provide },
});
-
- if (!isLoading) {
- return waitForPromises();
- }
};
- beforeEach(() => {
- mockVariables = jest.fn();
- });
-
afterEach(() => {
wrapper.destroy();
});
- describe('while queries are being fetch', () => {
+ describe('Props', () => {
beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
+ createComponent();
});
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
+ it('are passed down the correctly to ci_variable_shared', () => {
+ expect(findCiShared().props()).toEqual({
+ id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId),
+ areScopedVariablesAvailable: false,
+ componentName: 'GroupVariables',
+ fullPath: mockProvide.groupPath,
+ hideEnvironmentScope: false,
+ mutationData: wrapper.vm.$options.mutationData,
+ queryData: wrapper.vm.$options.queryData,
+ refetchAfterMutation: false,
+ });
});
});
- describe('when queries are resolved', () => {
- describe('successfuly', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
-
- await createComponentWithApollo();
- });
-
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([]);
- });
-
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockGroupVariables.data.group.ciVariables.nodes,
- );
+ describe('feature flag', () => {
+ describe('When enabled', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { groupScopedCiVariables: true } } });
});
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
+ it('Passes down `true` to variable shared component', () => {
+ expect(findCiShared().props('areScopedVariablesAvailable')).toBe(true);
});
});
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockVariables.mockRejectedValue();
-
- await createComponentWithApollo();
+ describe('When disabled', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { groupScopedCiVariables: false } } });
});
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('Passes down `false` to variable shared component', () => {
+ expect(findCiShared().props('areScopedVariablesAvailable')).toBe(false);
});
});
});
-
- describe('mutations', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
-
- await createComponentWithApollo();
- });
- it.each`
- actionName | mutation | event
- ${'add'} | ${addGroupVariable} | ${'add-variable'}
- ${'update'} | ${updateGroupVariable} | ${'update-variable'}
- ${'delete'} | ${deleteGroupVariable} | ${'delete-variable'}
- `(
- 'calls the right mutation when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- fullPath: mockProvide.groupPath,
- groupId: convertToGraphQLId('Group', mockProvide.groupId),
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event | mutationName
- ${'add'} | ${'add-variable'} | ${'addGroupVariable'}
- ${'update'} | ${'update-variable'} | ${'updateGroupVariable'}
- ${'delete'} | ${'delete-variable'} | ${'deleteGroupVariable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event, mutationName }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
- });
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
- });
});
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 c630278fbde..97051325f59 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
@@ -1,215 +1,45 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/settings';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciProjectVariables from '~/ci_variable_list/components/ci_project_variables.vue';
-import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
-import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
-import addProjectVariable from '~/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
-import deleteProjectVariable from '~/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
-import updateProjectVariable from '~/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
-
-import {
- environmentFetchErrorText,
- genericMutationErrorText,
- variableFetchErrorText,
-} from '~/ci_variable_list/constants';
-
-import {
- devName,
- mockProjectEnvironments,
- mockProjectVariables,
- newVariable,
- prodName,
-} from '../mocks';
-
-jest.mock('~/flash');
-
-Vue.use(VueApollo);
+import { GRAPHQL_PROJECT_TYPE } from '~/ci_variable_list/constants';
const mockProvide = {
- endpoint: '/variables',
projectFullPath: '/namespace/project',
projectId: 1,
};
-describe('Ci Project Variable list', () => {
+describe('Ci Project Variable wrapper', () => {
let wrapper;
- let mockApollo;
- let mockEnvironments;
- let mockVariables;
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiTable = () => wrapper.findComponent(GlTable);
- const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
-
- // eslint-disable-next-line consistent-return
- const createComponentWithApollo = async ({ isLoading = false } = {}) => {
- const handlers = [
- [getProjectEnvironments, mockEnvironments],
- [getProjectVariables, mockVariables],
- ];
-
- mockApollo = createMockApollo(handlers, resolvers);
+ const findCiShared = () => wrapper.findComponent(ciVariableShared);
+ const createComponent = () => {
wrapper = shallowMount(ciProjectVariables, {
provide: mockProvide,
- apolloProvider: mockApollo,
- stubs: { ciVariableSettings, ciVariableTable },
});
-
- if (!isLoading) {
- return waitForPromises();
- }
};
beforeEach(() => {
- mockEnvironments = jest.fn();
- mockVariables = jest.fn();
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
});
- describe('while queries are being fetch', () => {
- beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
- });
-
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
- });
- });
-
- describe('when queries are resolved', () => {
- describe('successfuly', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
-
- await createComponentWithApollo();
- });
-
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
- });
-
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockProjectVariables.data.project.ciVariables.nodes,
- );
- });
-
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
-
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockRejectedValue();
-
- await createComponentWithApollo();
- });
-
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
- });
- });
-
- describe('with an error for environments', () => {
- beforeEach(async () => {
- mockEnvironments.mockRejectedValue();
- mockVariables.mockResolvedValue(mockProjectVariables);
-
- await createComponentWithApollo();
- });
-
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
- });
- });
- });
-
- describe('mutations', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
-
- await createComponentWithApollo();
+ it('Passes down the correct props to ci_variable_shared', () => {
+ expect(findCiShared().props()).toEqual({
+ id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId),
+ areScopedVariablesAvailable: true,
+ componentName: 'ProjectVariables',
+ fullPath: mockProvide.projectFullPath,
+ hideEnvironmentScope: false,
+ mutationData: wrapper.vm.$options.mutationData,
+ queryData: wrapper.vm.$options.queryData,
+ refetchAfterMutation: false,
});
- it.each`
- actionName | mutation | event
- ${'add'} | ${addProjectVariable} | ${'add-variable'}
- ${'update'} | ${updateProjectVariable} | ${'update-variable'}
- ${'delete'} | ${deleteProjectVariable} | ${'delete-variable'}
- `(
- 'calls the right mutation when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- fullPath: mockProvide.projectFullPath,
- projectId: convertToGraphQLId('Project', mockProvide.projectId),
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event | mutationName
- ${'add'} | ${'add-variable'} | ${'addProjectVariable'}
- ${'update'} | ${'update-variable'} | ${'updateProjectVariable'}
- ${'delete'} | ${'delete-variable'} | ${'deleteProjectVariable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event, mutationName }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
- });
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index 1ea4e4f833b..e4771f040d1 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
@@ -39,6 +39,7 @@ describe('Ci variable modal', () => {
const defaultProps = {
areScopedVariablesAvailable: true,
environments: [],
+ hideEnvironmentScope: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
variable: [],
@@ -75,6 +76,7 @@ describe('Ci variable modal', () => {
const findEnvScopeInput = () =>
wrapper.findByTestId('environment-scope').findComponent(GlFormInput);
const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
+ const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
afterEach(() => {
wrapper.destroy();
@@ -250,39 +252,83 @@ describe('Ci variable modal', () => {
describe('Environment scope', () => {
describe('when feature is available', () => {
- it('renders the environment dropdown', () => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: true,
- },
+ describe('and section is not hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: false,
+ },
+ });
});
- expect(findCiEnvironmentsDropdown().exists()).toBe(true);
- expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
- });
+ it('renders the environment dropdown and section title', () => {
+ expect(findCiEnvironmentsDropdown().exists()).toBe(true);
+ expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
+ expect(findEnvironmentScopeText().exists()).toBe(true);
+ });
- it('renders a link to documentation on scopes', () => {
- createComponent({ mountFn: mountExtended });
+ it('renders a link to documentation on scopes', () => {
+ const link = findEnvScopeLink();
+
+ expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
+ expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
+ });
+ });
- const link = findEnvScopeLink();
+ describe('and section is hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: true,
+ },
+ });
+ });
- expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
- expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
+ it('does not renders the environment dropdown and section title', () => {
+ expect(findCiEnvironmentsDropdown().exists()).toBe(false);
+ expect(findEnvironmentScopeText().exists()).toBe(false);
+ });
});
});
describe('when feature is not available', () => {
- it('disables the dropdown', () => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: false,
- },
+ describe('and section is not hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: false,
+ hideEnvironmentScope: false,
+ },
+ });
});
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(findEnvScopeInput().attributes('readonly')).toBe('readonly');
+ it('disables the dropdown', () => {
+ expect(findCiEnvironmentsDropdown().exists()).toBe(false);
+ expect(findEnvironmentScopeText().exists()).toBe(true);
+ expect(findEnvScopeInput().attributes('readonly')).toBe('readonly');
+ });
+ });
+
+ describe('and section is hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: false,
+ hideEnvironmentScope: true,
+ },
+ });
+ });
+
+ it('hides the dropdown', () => {
+ expect(findEnvironmentScopeText().exists()).toBe(false);
+ expect(findCiEnvironmentsDropdown().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
deleted file mode 100644
index 4d0c378d10e..00000000000
--- a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import CiVariablePopover from '~/ci_variable_list/components/ci_variable_popover.vue';
-import mockData from '../services/mock_data';
-
-describe('Ci Variable Popover', () => {
- let wrapper;
-
- const defaultProps = {
- target: 'ci-variable-value-22',
- value: mockData.mockPemCert,
- tooltipText: 'Copy value',
- };
-
- const createComponent = (props = defaultProps) => {
- wrapper = shallowMount(CiVariablePopover, {
- propsData: { ...props },
- });
- };
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('displays max count plus ... when character count is over 95', () => {
- expect(wrapper.text()).toHaveLength(98);
- });
-
- it('copies full value to clipboard', () => {
- expect(findButton().attributes('data-clipboard-text')).toEqual(mockData.mockPemCert);
- });
-
- it('displays full value when count is less than max count', () => {
- createComponent({
- target: 'ci-variable-value-22',
- value: 'test_variable_value',
- tooltipText: 'Copy value',
- });
- expect(wrapper.text()).toEqual('test_variable_value');
- });
-});
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 5c77ce71b41..8b5a0f7ae9d 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
@@ -18,6 +18,7 @@ describe('Ci variable table', () => {
const defaultProps = {
areScopedVariablesAvailable: true,
environments: mapEnvironmentNames(mockEnvs),
+ hideEnvironmentScope: false,
isLoading: false,
variables: mockVariablesWithScopes(projectString),
};
@@ -56,6 +57,7 @@ describe('Ci variable table', () => {
expect(findCiVariableModal().props()).toEqual({
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
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
new file mode 100644
index 00000000000..0cc0ee7a9c7
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
@@ -0,0 +1,428 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/settings';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
+import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+ environmentFetchErrorText,
+ genericMutationErrorText,
+ variableFetchErrorText,
+} from '~/ci_variable_list/constants';
+
+import {
+ createGroupProps,
+ createInstanceProps,
+ createProjectProps,
+ devName,
+ mockProjectEnvironments,
+ mockProjectVariables,
+ newVariable,
+ prodName,
+ mockGroupVariables,
+ mockAdminVariables,
+} from '../mocks';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const mockProvide = {
+ endpoint: '/variables',
+};
+
+const defaultProps = {
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: false,
+ refetchAfterMutation: false,
+};
+
+describe('Ci Variable Shared Component', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockEnvironments;
+ let mockVariables;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCiTable = () => wrapper.findComponent(GlTable);
+ const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
+
+ // eslint-disable-next-line consistent-return
+ async function createComponentWithApollo({
+ customHandlers = null,
+ isLoading = false,
+ props = { ...createProjectProps() },
+ } = {}) {
+ const handlers = customHandlers || [
+ [getProjectEnvironments, mockEnvironments],
+ [getProjectVariables, mockVariables],
+ ];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = shallowMount(ciVariableShared, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: mockProvide,
+ apolloProvider: mockApollo,
+ stubs: { ciVariableSettings, ciVariableTable },
+ });
+
+ if (!isLoading) {
+ return waitForPromises();
+ }
+ }
+
+ beforeEach(() => {
+ mockEnvironments = jest.fn();
+ mockVariables = jest.fn();
+ });
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries are resolved', () => {
+ describe('successfuly', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
+ });
+
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockProjectVariables.data.project.ciVariables.nodes,
+ );
+ });
+
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockRejectedValue();
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
+ });
+
+ describe('with an error for environments', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockRejectedValue();
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ });
+ });
+ });
+
+ describe('environment query', () => {
+ describe('when there is an environment key in queryData', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo({ props: { ...createProjectProps() } });
+ });
+
+ it('is executed', () => {
+ expect(mockVariables).toHaveBeenCalled();
+ });
+ });
+
+ describe('when there isnt an environment key in queryData', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo({ props: { ...createGroupProps() } });
+ });
+
+ it('is skipped', () => {
+ expect(mockVariables).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ const groupProps = createGroupProps();
+
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: groupProps,
+ });
+ });
+ it.each`
+ actionName | mutation | event
+ ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'}
+ ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'}
+ ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'}
+ `(
+ 'calls the right mutation from propsData when user performs $actionName variable',
+ async ({ event, mutation }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ endpoint: mockProvide.endpoint,
+ fullPath: groupProps.fullPath,
+ id: convertToGraphQLId('Group', groupProps.id),
+ variable: newVariable,
+ },
+ });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error on failure with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+
+ describe('without fullpath and ID props', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ props: createInstanceProps(),
+ });
+ });
+
+ it('does not pass fullPath and ID to the mutation', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION],
+ variables: {
+ endpoint: mockProvide.endpoint,
+ variable: newVariable,
+ },
+ });
+ });
+ });
+ });
+
+ describe('Props', () => {
+ 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}
+ `(
+ 'passes down all the required props when its a $name component',
+ async ({
+ mutation,
+ mockVariablesValue,
+ mockEnvironmentsValue,
+ withEnvironments,
+ expectedEnvironments,
+ propsFn,
+ }) => {
+ const props = propsFn();
+
+ mockVariables.mockResolvedValue(mockVariablesValue);
+
+ if (withEnvironments) {
+ mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
+ }
+
+ let customHandlers = null;
+
+ if (mutation) {
+ customHandlers = [[mutation, mockVariables]];
+ }
+
+ await createComponentWithApollo({ customHandlers, props });
+
+ expect(findCiSettings().props()).toEqual({
+ areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ isLoading: false,
+ variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes,
+ environments: expectedEnvironments,
+ });
+ },
+ );
+ });
+
+ describe('refetchAfterMutation', () => {
+ it.each`
+ bool | text
+ ${true} | ${'refetches the variables'}
+ ${false} | ${'does not refetch the variables'}
+ `('when $bool it $text', async ({ bool }) => {
+ await createComponentWithApollo({
+ props: { ...createInstanceProps(), refetchAfterMutation: bool },
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
+ jest.spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch').mockImplementation(jest.fn());
+
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+
+ await nextTick();
+
+ if (bool) {
+ expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled();
+ } else {
+ expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled();
+ }
+ });
+ });
+
+ describe('Validators', () => {
+ describe('queryData', () => {
+ let error;
+
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps() },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('will not mount component with wrong data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps(), queryData: { wrongKey: {} } },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(false);
+ expect(error.toString()).toContain('custom validator check failed for prop');
+ }
+ });
+ });
+
+ describe('mutationData', () => {
+ let error;
+
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('will not mount component with wrong data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(false);
+ expect(error.toString()).toContain('custom validator check failed for prop');
+ }
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
deleted file mode 100644
index b3e23ba4201..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import LegacyCiEnvironmentsDropdown from '~/ci_variable_list/components/legacy_ci_environments_dropdown.vue';
-
-Vue.use(Vuex);
-
-describe('Ci environments dropdown', () => {
- let wrapper;
- let store;
-
- const enterSearchTerm = (value) =>
- wrapper.find('[data-testid="ci-environment-search"]').setValue(value);
-
- const createComponent = (term) => {
- store = new Vuex.Store({
- getters: {
- joinedEnvironments: () => ['dev', 'prod', 'staging'],
- },
- });
-
- wrapper = mount(LegacyCiEnvironmentsDropdown, {
- store,
- propsData: {
- value: term,
- },
- });
- enterSearchTerm(term);
- };
-
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('No environments found', () => {
- beforeEach(() => {
- createComponent('stable');
- });
-
- it('renders create button with search term if environments do not contain search term', () => {
- expect(findAllDropdownItems()).toHaveLength(2);
- expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
- });
-
- it('renders empty results message', () => {
- expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
- });
- });
-
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent('');
- });
-
- it('renders all environments when search term is empty', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe('dev');
- expect(findDropdownItemByIndex(1).text()).toBe('prod');
- expect(findDropdownItemByIndex(2).text()).toBe('staging');
- });
-
- it('should not display active checkmark on the inactive stage', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
- });
-
- describe('Environments found', () => {
- beforeEach(async () => {
- createComponent('prod');
- await nextTick();
- });
-
- it('renders only the environment searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('prod');
- });
-
- it('should not display create button', () => {
- const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create'));
- expect(environments).toHaveLength(0);
- expect(findAllDropdownItems()).toHaveLength(1);
- });
-
- it('should not display empty results message', () => {
- expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false);
- });
-
- it('should display active checkmark if active', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(false);
- });
-
- it('should clear the search term when showing the dropdown', () => {
- wrapper.findComponent(GlDropdown).trigger('click');
-
- expect(wrapper.find('[data-testid="ci-environment-search"]').text()).toBe('');
- });
-
- describe('Custom events', () => {
- it('should emit selectEnvironment if an environment is clicked', () => {
- findDropdownItemByIndex(0).vm.$emit('click');
- expect(wrapper.emitted('selectEnvironment')).toEqual([['prod']]);
- });
-
- it('should emit createClicked if an environment is clicked', async () => {
- createComponent('newscope');
-
- await nextTick();
- findDropdownItemByIndex(1).vm.$emit('click');
- expect(wrapper.emitted('createClicked')).toEqual([['newscope']]);
- });
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
deleted file mode 100644
index b607232907b..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
+++ /dev/null
@@ -1,323 +0,0 @@
-import { GlButton, GlFormInput } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mockTracking } from 'helpers/tracking_helper';
-import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
-import LegacyCiVariableModal from '~/ci_variable_list/components/legacy_ci_variable_modal.vue';
-import {
- AWS_ACCESS_KEY_ID,
- EVENT_LABEL,
- EVENT_ACTION,
- ENVIRONMENT_SCOPE_LINK_TITLE,
-} from '~/ci_variable_list/constants';
-import createStore from '~/ci_variable_list/store';
-import mockData from '../services/mock_data';
-import ModalStub from '../stubs';
-
-Vue.use(Vuex);
-
-describe('Ci variable modal', () => {
- let wrapper;
- let store;
- let trackingSpy;
-
- const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
-
- const createComponent = (method, options = {}) => {
- store = createStore({
- maskableRegex,
- isGroup: options.isGroup,
- environmentScopeLink: '/help/environments',
- });
- wrapper = method(LegacyCiVariableModal, {
- attachTo: document.body,
- stubs: {
- GlModal: ModalStub,
- },
- store,
- ...options,
- });
- };
-
- const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
- const findModal = () => wrapper.findComponent(ModalStub);
- const findAddorUpdateButton = () => findModal().find('[data-testid="ciUpdateOrAddVariableBtn"]');
- const deleteVariableButton = () =>
- findModal()
- .findAllComponents(GlButton)
- .wrappers.find((button) => button.props('variant') === 'danger');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Basic interactions', () => {
- beforeEach(() => {
- createComponent(shallowMount);
- });
-
- it('button is disabled when no key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
- });
- });
-
- describe('Adding a new variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- createComponent(shallowMount);
- jest.spyOn(store, 'dispatch').mockImplementation();
- store.state.variable = variable;
- });
-
- it('button is enabled when key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
-
- it('Add variable button dispatches addVariable action', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('addVariable');
- });
-
- it('Clears the modal state once modal is hidden', () => {
- findModal().vm.$emit('hidden');
- expect(store.dispatch).toHaveBeenCalledWith('clearModal');
- });
-
- it('should dispatch setVariableProtected when admin settings are configured to protect variables', () => {
- store.state.isProtectedByDefault = true;
- findModal().vm.$emit('shown');
-
- expect(store.dispatch).toHaveBeenCalledWith('setVariableProtected');
- });
- });
-
- describe('Adding a new non-AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value: 'value',
- secret_value: 'secret_value',
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- });
-
- it('does not show AWS guidance tip', () => {
- const tip = wrapper.find(`div[data-testid='aws-guidance-tip']`);
- expect(tip.exists()).toBe(true);
- expect(tip.isVisible()).toBe(false);
- });
- });
-
- describe('Adding a new AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- secret_value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- });
-
- it('shows AWS guidance tip', () => {
- const tip = wrapper.find(`[data-testid='aws-guidance-tip']`);
- expect(tip.exists()).toBe(true);
- expect(tip.isVisible()).toBe(true);
- });
- });
-
- describe.each`
- value | secret | rendered
- ${'value'} | ${'secret_value'} | ${false}
- ${'dollar$ign'} | ${'dollar$ign'} | ${true}
- `('Adding a new variable', ({ value, secret, rendered }) => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value,
- secret_value: secret,
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it(`${rendered ? 'renders' : 'does not render'} the variable reference warning`, () => {
- const warning = wrapper.find(`[data-testid='contains-variable-reference']`);
- expect(warning.exists()).toBe(rendered);
- });
- });
-
- describe('Editing a variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- createComponent(shallowMount);
- jest.spyOn(store, 'dispatch').mockImplementation();
- store.state.variableBeingEdited = variable;
- });
-
- it('button text is Update variable when updating', () => {
- expect(findAddorUpdateButton().text()).toBe('Update variable');
- });
-
- it('Update variable button dispatches updateVariable with correct variable', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('updateVariable');
- });
-
- it('Resets the editing state once modal is hidden', () => {
- findModal().vm.$emit('hidden');
- expect(store.dispatch).toHaveBeenCalledWith('resetEditing');
- });
-
- it('dispatches deleteVariable with correct variable to delete', () => {
- deleteVariableButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('deleteVariable');
- });
- });
-
- describe('Environment scope', () => {
- describe('group level variables', () => {
- it('renders the environment dropdown', () => {
- createComponent(shallowMount, {
- isGroup: true,
- provide: {
- glFeatures: {
- groupScopedCiVariables: true,
- },
- },
- });
-
- expect(findCiEnvironmentsDropdown().exists()).toBe(true);
- expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
- });
-
- describe('licensed feature is not available', () => {
- it('disables the dropdown', () => {
- createComponent(mount, {
- isGroup: true,
- provide: {
- glFeatures: {
- groupScopedCiVariables: false,
- },
- },
- });
-
- const environmentScopeInput = wrapper
- .find('[data-testid="environment-scope"]')
- .findComponent(GlFormInput);
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(environmentScopeInput.attributes('readonly')).toBe('readonly');
- });
- });
- });
-
- it('renders a link to documentation on scopes', () => {
- createComponent(mount);
-
- const link = wrapper.find('[data-testid="environment-scope-link"]');
-
- expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
- expect(link.attributes('href')).toBe('/help/environments');
- });
- });
-
- describe('Validations', () => {
- const maskError = 'This variable can not be masked.';
-
- describe('when the mask state is invalid', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidMaskVariable = {
- ...variable,
- key: 'qs',
- value: 'd:;',
- secret_value: 'd:;',
- masked: true,
- };
- createComponent(mount);
- store.state.variable = invalidMaskVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
- });
-
- it('shows the correct error text', () => {
- expect(findModal().text()).toContain(maskError);
- });
-
- it('sends the correct tracking event', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: ';',
- });
- });
- });
-
- describe.each`
- value | secret | masked | eventSent | trackingErrorProperty
- ${'value'} | ${'secretValue'} | ${false} | ${0} | ${null}
- ${'shortMasked'} | ${'short'} | ${true} | ${0} | ${null}
- ${'withDollar$Sign'} | ${'dollar$ign'} | ${false} | ${1} | ${'$'}
- ${'withDollar$Sign'} | ${'dollar$ign'} | ${true} | ${1} | ${'$'}
- ${'unsupported'} | ${'unsupported|char'} | ${true} | ${1} | ${'|'}
- ${'unsupportedMasked'} | ${'unsupported|char'} | ${false} | ${0} | ${null}
- `('Adding a new variable', ({ value, secret, masked, eventSent, trackingErrorProperty }) => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value,
- secret_value: secret,
- masked,
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it(`${
- eventSent > 0 ? 'sends the correct' : 'does not send the'
- } variable validation tracking event`, () => {
- expect(trackingSpy).toHaveBeenCalledTimes(eventSent);
-
- if (eventSent > 0) {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: trackingErrorProperty,
- });
- }
- });
- });
-
- describe('when both states are valid', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const validMaskandKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: '12345678',
- secret_value: '87654321',
- masked: true,
- };
- createComponent(mount);
- store.state.variable = validMaskandKeyVariable;
- });
-
- it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
deleted file mode 100644
index 7def4dd4f29..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import LegacyCiVariableSettings from '~/ci_variable_list/components/legacy_ci_variable_settings.vue';
-import createStore from '~/ci_variable_list/store';
-
-Vue.use(Vuex);
-
-describe('Ci variable table', () => {
- let wrapper;
- let store;
- let isProject;
-
- const createComponent = (projectState) => {
- store = createStore();
- store.state.isProject = projectState;
- jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = shallowMount(LegacyCiVariableSettings, {
- store,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches fetchEnvironments when mounted', () => {
- isProject = true;
- createComponent(isProject);
- expect(store.dispatch).toHaveBeenCalledWith('fetchEnvironments');
- });
-
- it('does not dispatch fetchenvironments when in group context', () => {
- isProject = false;
- createComponent(isProject);
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
deleted file mode 100644
index 310afc8003a..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import LegacyCiVariableTable from '~/ci_variable_list/components/legacy_ci_variable_table.vue';
-import createStore from '~/ci_variable_list/store';
-import mockData from '../services/mock_data';
-
-Vue.use(Vuex);
-
-describe('Ci variable table', () => {
- let wrapper;
- let store;
-
- const createComponent = () => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = mountExtended(LegacyCiVariableTable, {
- attachTo: document.body,
- store,
- });
- };
-
- const findRevealButton = () => wrapper.findByText('Reveal values');
- const findEditButton = () => wrapper.findByLabelText('Edit');
- const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches fetchVariables when mounted', () => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
- });
-
- describe('When table is empty', () => {
- beforeEach(() => {
- store.state.variables = [];
- });
-
- it('displays empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
- });
-
- it('hides the reveal button', () => {
- expect(findRevealButton().exists()).toBe(false);
- });
- });
-
- describe('When table has variables', () => {
- beforeEach(() => {
- store.state.variables = mockData.mockVariables;
- });
-
- it('does not display the empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
- });
-
- it('displays the reveal button', () => {
- expect(findRevealButton().exists()).toBe(true);
- });
-
- it('displays the correct amount of variables', async () => {
- expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(1);
- });
- });
-
- describe('Table click actions', () => {
- beforeEach(() => {
- store.state.variables = mockData.mockVariables;
- });
-
- it('reveals secret values when button is clicked', () => {
- findRevealButton().trigger('click');
- expect(store.dispatch).toHaveBeenCalledWith('toggleValues', false);
- });
-
- it('dispatches editVariable with correct variable to edit', () => {
- findEditButton().trigger('click');
- expect(store.dispatch).toHaveBeenCalledWith('editVariable', mockData.mockVariables[0]);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 6f3e73f8b83..03b77f80430 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -1,10 +1,28 @@
import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
variableTypes,
groupString,
instanceString,
projectString,
} from '~/ci_variable_list/constants';
+import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
+import addProjectVariable from '~/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
+import deleteProjectVariable from '~/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
+import updateProjectVariable from '~/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
+
+import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+
export const devName = 'dev';
export const prodName = 'prod';
@@ -118,3 +136,62 @@ export const newVariable = {
value: 'devops',
variableType: variableTypes.variableType,
};
+
+export const createProjectProps = () => {
+ return {
+ componentName: 'ProjectVariable',
+ fullPath: '/namespace/project/',
+ id: 'gid://gitlab/Project/20',
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addProjectVariable,
+ [UPDATE_MUTATION_ACTION]: updateProjectVariable,
+ [DELETE_MUTATION_ACTION]: deleteProjectVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.project?.ciVariables,
+ query: getProjectVariables,
+ },
+ environments: {
+ lookup: (data) => data?.project?.environments,
+ query: getProjectEnvironments,
+ },
+ },
+ };
+};
+
+export const createGroupProps = () => {
+ return {
+ componentName: 'GroupVariable',
+ fullPath: '/my-group',
+ id: 'gid://gitlab/Group/20',
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addGroupVariable,
+ [UPDATE_MUTATION_ACTION]: updateGroupVariable,
+ [DELETE_MUTATION_ACTION]: deleteGroupVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.group?.ciVariables,
+ query: getGroupVariables,
+ },
+ },
+ };
+};
+
+export const createInstanceProps = () => {
+ return {
+ componentName: 'InstanceVariable',
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addAdminVariable,
+ [UPDATE_MUTATION_ACTION]: updateAdminVariable,
+ [DELETE_MUTATION_ACTION]: deleteAdminVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.ciVariables,
+ query: getAdminVariables,
+ },
+ },
+ };
+};
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
deleted file mode 100644
index e8c81a53a55..00000000000
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ /dev/null
@@ -1,319 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import Api from '~/api';
-import * as actions from '~/ci_variable_list/store/actions';
-import * as types from '~/ci_variable_list/store/mutation_types';
-import getInitialState from '~/ci_variable_list/store/state';
-import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import mockData from '../services/mock_data';
-
-jest.mock('~/api.js');
-jest.mock('~/flash.js');
-
-describe('CI variable list store actions', () => {
- let mock;
- let state;
- const mockVariable = {
- environment_scope: '*',
- id: 63,
- key: 'test_var',
- masked: false,
- protected: false,
- value: 'test_val',
- variable_type: 'env_var',
- _destory: true,
- };
- const payloadError = new Error('Request failed with status code 500');
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- state = getInitialState();
- state.endpoint = '/variables';
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('toggleValues', () => {
- const valuesHidden = false;
- it('commits TOGGLE_VALUES mutation', () => {
- testAction(actions.toggleValues, valuesHidden, {}, [
- {
- type: types.TOGGLE_VALUES,
- payload: valuesHidden,
- },
- ]);
- });
- });
-
- describe('clearModal', () => {
- it('commits CLEAR_MODAL mutation', () => {
- testAction(actions.clearModal, {}, {}, [
- {
- type: types.CLEAR_MODAL,
- },
- ]);
- });
- });
-
- describe('resetEditing', () => {
- it('commits RESET_EDITING mutation', () => {
- testAction(
- actions.resetEditing,
- {},
- {},
- [
- {
- type: types.RESET_EDITING,
- },
- ],
- [{ type: 'fetchVariables' }],
- );
- });
- });
-
- describe('setVariableProtected', () => {
- it('commits SET_VARIABLE_PROTECTED mutation', () => {
- testAction(actions.setVariableProtected, {}, {}, [
- {
- type: types.SET_VARIABLE_PROTECTED,
- },
- ]);
- });
- });
-
- describe('deleteVariable', () => {
- it('dispatch correct actions on successful deleted variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.deleteVariable,
- {},
- state,
- [],
- [
- { type: 'requestDeleteVariable' },
- { type: 'receiveDeleteVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on delete failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.deleteVariable,
- {},
- state,
- [],
- [
- { type: 'requestDeleteVariable' },
- {
- type: 'receiveDeleteVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('updateVariable', () => {
- it('dispatch correct actions on successful updated variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.updateVariable,
- {},
- state,
- [],
- [
- { type: 'requestUpdateVariable' },
- { type: 'receiveUpdateVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on update failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.updateVariable,
- mockVariable,
- state,
- [],
- [
- { type: 'requestUpdateVariable' },
- {
- type: 'receiveUpdateVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('addVariable', () => {
- it('dispatch correct actions on successful added variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.addVariable,
- {},
- state,
- [],
- [
- { type: 'requestAddVariable' },
- { type: 'receiveAddVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on add failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.addVariable,
- {},
- state,
- [],
- [
- { type: 'requestAddVariable' },
- {
- type: 'receiveAddVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('fetchVariables', () => {
- it('dispatch correct actions on fetchVariables', () => {
- mock.onGet(state.endpoint).reply(200, { variables: mockData.mockVariables });
-
- return testAction(
- actions.fetchVariables,
- {},
- state,
- [],
- [
- { type: 'requestVariables' },
- {
- type: 'receiveVariablesSuccess',
- payload: prepareDataForDisplay(mockData.mockVariables),
- },
- ],
- );
- });
-
- it('should show flash error and set error in state on fetch variables failure', async () => {
- mock.onGet(state.endpoint).reply(500);
-
- await testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }]);
- expect(createAlert).toHaveBeenCalledWith({
- message: 'There was an error fetching the variables.',
- });
- });
- });
-
- describe('fetchEnvironments', () => {
- it('dispatch correct actions on fetchEnvironments', () => {
- Api.environments = jest.fn().mockResolvedValue({ data: mockData.mockEnvironments });
-
- return testAction(
- actions.fetchEnvironments,
- {},
- state,
- [],
- [
- { type: 'requestEnvironments' },
- {
- type: 'receiveEnvironmentsSuccess',
- payload: prepareEnvironments(mockData.mockEnvironments),
- },
- ],
- );
- });
-
- it('should show flash error and set error in state on fetch environments failure', async () => {
- Api.environments = jest.fn().mockRejectedValue();
-
- await testAction(actions.fetchEnvironments, {}, state, [], [{ type: 'requestEnvironments' }]);
-
- expect(createAlert).toHaveBeenCalledWith({
- message: 'There was an error fetching the environments information.',
- });
- });
- });
-
- describe('Update variable values', () => {
- it('updateVariableKey', () => {
- testAction(
- actions.updateVariableKey,
- { key: mockVariable.key },
- {},
- [
- {
- type: types.UPDATE_VARIABLE_KEY,
- payload: mockVariable.key,
- },
- ],
- [],
- );
- });
-
- it('updateVariableValue', () => {
- testAction(
- actions.updateVariableValue,
- { secret_value: mockVariable.value },
- {},
- [
- {
- type: types.UPDATE_VARIABLE_VALUE,
- payload: mockVariable.value,
- },
- ],
- [],
- );
- });
-
- it('updateVariableType', () => {
- testAction(
- actions.updateVariableType,
- { variable_type: mockVariable.variable_type },
- {},
- [{ type: types.UPDATE_VARIABLE_TYPE, payload: mockVariable.variable_type }],
- [],
- );
- });
-
- it('updateVariableProtected', () => {
- testAction(
- actions.updateVariableProtected,
- { protected_variable: mockVariable.protected },
- {},
- [{ type: types.UPDATE_VARIABLE_PROTECTED, payload: mockVariable.protected }],
- [],
- );
- });
-
- it('updateVariableMasked', () => {
- testAction(
- actions.updateVariableMasked,
- { masked: mockVariable.masked },
- {},
- [{ type: types.UPDATE_VARIABLE_MASKED, payload: mockVariable.masked }],
- [],
- );
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/getters_spec.js b/spec/frontend/ci_variable_list/store/getters_spec.js
deleted file mode 100644
index 92f22b18763..00000000000
--- a/spec/frontend/ci_variable_list/store/getters_spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as getters from '~/ci_variable_list/store/getters';
-import mockData from '../services/mock_data';
-
-describe('Ci variable getters', () => {
- describe('joinedEnvironments', () => {
- it('should join fetched environments with variable environment scopes', () => {
- const state = {
- environments: ['All (default)', 'staging', 'deployment', 'prod'],
- variables: mockData.mockVariableScopes,
- };
-
- expect(getters.joinedEnvironments(state)).toEqual([
- 'All (default)',
- 'deployment',
- 'prod',
- 'production',
- 'staging',
- ]);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
deleted file mode 100644
index c7d07ead09b..00000000000
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import * as types from '~/ci_variable_list/store/mutation_types';
-import mutations from '~/ci_variable_list/store/mutations';
-import state from '~/ci_variable_list/store/state';
-
-describe('CI variable list mutations', () => {
- let stateCopy;
-
- beforeEach(() => {
- stateCopy = state();
- });
-
- describe('TOGGLE_VALUES', () => {
- it('should toggle state', () => {
- const valuesHidden = false;
-
- mutations[types.TOGGLE_VALUES](stateCopy, valuesHidden);
-
- expect(stateCopy.valuesHidden).toEqual(valuesHidden);
- });
- });
-
- describe('VARIABLE_BEING_EDITED', () => {
- it('should set the variable that is being edited', () => {
- mutations[types.VARIABLE_BEING_EDITED](stateCopy);
-
- expect(stateCopy.variableBeingEdited).toBe(true);
- });
- });
-
- describe('RESET_EDITING', () => {
- it('should reset variableBeingEdited to false', () => {
- mutations[types.RESET_EDITING](stateCopy);
-
- expect(stateCopy.variableBeingEdited).toBe(false);
- });
- });
-
- describe('CLEAR_MODAL', () => {
- it('should clear modal state', () => {
- const modalState = {
- variable_type: 'Variable',
- key: '',
- secret_value: '',
- protected_variable: false,
- masked: false,
- environment_scope: 'All (default)',
- };
-
- mutations[types.CLEAR_MODAL](stateCopy);
-
- expect(stateCopy.variable).toEqual(modalState);
- });
- });
-
- describe('RECEIVE_ENVIRONMENTS_SUCCESS', () => {
- it('should set environments', () => {
- const environments = ['env1', 'env2'];
-
- mutations[types.RECEIVE_ENVIRONMENTS_SUCCESS](stateCopy, environments);
-
- expect(stateCopy.environments).toEqual(['All (default)', 'env1', 'env2']);
- });
- });
-
- describe('SET_ENVIRONMENT_SCOPE', () => {
- const environment = 'production';
-
- it('should set environment scope on variable', () => {
- mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
-
- expect(stateCopy.variable.environment_scope).toBe('production');
- });
- });
-
- describe('ADD_WILD_CARD_SCOPE', () => {
- it('should add wild card scope to environments array and sort', () => {
- stateCopy.environments = ['dev', 'staging'];
- mutations[types.ADD_WILD_CARD_SCOPE](stateCopy, 'production');
-
- expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']);
- });
- });
-
- describe('SET_VARIABLE_PROTECTED', () => {
- it('should set protected value to true', () => {
- mutations[types.SET_VARIABLE_PROTECTED](stateCopy);
-
- expect(stateCopy.variable.protected_variable).toBe(true);
- });
- });
-
- describe('UPDATE_VARIABLE_KEY', () => {
- it('should update variable key value', () => {
- const key = 'new_var';
- mutations[types.UPDATE_VARIABLE_KEY](stateCopy, key);
-
- expect(stateCopy.variable.key).toBe(key);
- });
- });
-
- describe('UPDATE_VARIABLE_VALUE', () => {
- it('should update variable value', () => {
- const value = 'variable_value';
- mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, value);
-
- expect(stateCopy.variable.secret_value).toBe(value);
- });
- });
-
- describe('UPDATE_VARIABLE_TYPE', () => {
- it('should update variable type value', () => {
- const type = 'File';
- mutations[types.UPDATE_VARIABLE_TYPE](stateCopy, type);
-
- expect(stateCopy.variable.variable_type).toBe(type);
- });
- });
-
- describe('UPDATE_VARIABLE_PROTECTED', () => {
- it('should update variable protected value', () => {
- const protectedValue = true;
- mutations[types.UPDATE_VARIABLE_PROTECTED](stateCopy, protectedValue);
-
- expect(stateCopy.variable.protected_variable).toBe(protectedValue);
- });
- });
-
- describe('UPDATE_VARIABLE_MASKED', () => {
- it('should update variable masked value', () => {
- const masked = true;
- mutations[types.UPDATE_VARIABLE_MASKED](stateCopy, masked);
-
- expect(stateCopy.variable.masked).toBe(masked);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/utils_spec.js b/spec/frontend/ci_variable_list/store/utils_spec.js
deleted file mode 100644
index 5b10370324a..00000000000
--- a/spec/frontend/ci_variable_list/store/utils_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- prepareDataForDisplay,
- prepareEnvironments,
- prepareDataForApi,
-} from '~/ci_variable_list/store/utils';
-import mockData from '../services/mock_data';
-
-describe('CI variables store utils', () => {
- it('prepares ci variables for display', () => {
- expect(prepareDataForDisplay(mockData.mockVariablesApi)).toStrictEqual(
- mockData.mockVariablesDisplay,
- );
- });
-
- it('prepares single ci variable for api', () => {
- expect(prepareDataForApi(mockData.mockVariablesDisplay[0])).toStrictEqual({
- environment_scope: '*',
- id: 113,
- key: 'test_var',
- masked: 'false',
- protected: 'false',
- secret_value: 'test_val',
- value: 'test_val',
- variable_type: 'env_var',
- });
-
- expect(prepareDataForApi(mockData.mockVariablesDisplay[1])).toStrictEqual({
- environment_scope: '*',
- id: 114,
- key: 'test_var_2',
- masked: 'false',
- protected: 'false',
- secret_value: 'test_val_2',
- value: 'test_val_2',
- variable_type: 'file',
- });
- });
-
- it('prepares single ci variable for delete', () => {
- expect(prepareDataForApi(mockData.mockVariablesDisplay[0], true)).toHaveProperty(
- '_destroy',
- true,
- );
- });
-
- it('prepares environments for display', () => {
- expect(prepareEnvironments(mockData.mockEnvironments)).toStrictEqual(['staging', 'production']);
- });
-});
diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
index cce17176129..98001858851 100644
--- a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
@@ -56,6 +56,7 @@ describe('content_editor/components/bubble_menus/formatting_bubble_menu', () =>
${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
${'superscript'} | ${{ contentType: 'superscript', iconName: 'superscript', label: 'Superscript', editorCommand: 'toggleSuperscript' }}
${'subscript'} | ${{ contentType: 'subscript', iconName: 'subscript', label: 'Subscript', editorCommand: 'toggleSubscript' }}
+ ${'highlight'} | ${{ contentType: 'highlight', iconName: 'highlight', label: 'Highlight', editorCommand: 'toggleHighlight' }}
${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link', editorCommand: 'toggleLink', editorCommandParams: { href: '' } }}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec.js b/spec/frontend/content_editor/markdown_snapshot_spec.js
index 63ca66172e6..146208bf8c7 100644
--- a/spec/frontend/content_editor/markdown_snapshot_spec.js
+++ b/spec/frontend/content_editor/markdown_snapshot_spec.js
@@ -1,10 +1,11 @@
-import path from 'path';
import { describeMarkdownSnapshots } from 'jest/content_editor/markdown_snapshot_spec_helper';
jest.mock('~/emoji');
-const glfmSpecificationDir = path.join(__dirname, '..', '..', '..', 'glfm_specification');
-
// See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
// for documentation on this spec.
-describeMarkdownSnapshots('CE markdown snapshots in ContentEditor', glfmSpecificationDir);
+//
+// NOTE: Unlike the backend markdown_snapshot_spec.rb which has a CE and EE version, there is only
+// one version of this spec. This is because the frontend markdown rendering does not require EE-only
+// backend features.
+describeMarkdownSnapshots('markdown example snapshots in ContentEditor');
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec_helper.js b/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
index 05fa8e6a6b2..64988c5b717 100644
--- a/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
+++ b/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
@@ -1,10 +1,12 @@
// See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
// for documentation on this spec.
-import fs from 'fs';
-import path from 'path';
import jsYaml from 'js-yaml';
import { pick } from 'lodash';
+import glfmExampleStatusYml from '../../../glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml';
+import markdownYml from '../../../glfm_specification/output_example_snapshots/markdown.yml';
+import htmlYml from '../../../glfm_specification/output_example_snapshots/html.yml';
+import prosemirrorJsonYml from '../../../glfm_specification/output_example_snapshots/prosemirror_json.yml';
import {
IMPLEMENTATION_ERROR_MSG,
renderHtmlAndJsonForAllExamples,
@@ -18,29 +20,21 @@ const filterExamples = (examples) => {
return pick(examples, focusedMarkdownExamples);
};
-const loadExamples = (dir, fileName) => {
- const yaml = fs.readFileSync(path.join(dir, fileName));
+const loadExamples = (yaml) => {
const examples = jsYaml.safeLoad(yaml, {});
return filterExamples(examples);
};
// eslint-disable-next-line jest/no-export
-export const describeMarkdownSnapshots = (description, glfmSpecificationDir) => {
+export const describeMarkdownSnapshots = (description) => {
let actualHtmlAndJsonExamples;
let skipRunningSnapshotWysiwygHtmlTests;
let skipRunningSnapshotProsemirrorJsonTests;
- const exampleStatuses = loadExamples(
- path.join(glfmSpecificationDir, 'input', 'gitlab_flavored_markdown'),
- 'glfm_example_status.yml',
- );
- const glfmExampleSnapshotsDir = path.join(glfmSpecificationDir, 'example_snapshots');
- const markdownExamples = loadExamples(glfmExampleSnapshotsDir, 'markdown.yml');
- const expectedHtmlExamples = loadExamples(glfmExampleSnapshotsDir, 'html.yml');
- const expectedProseMirrorJsonExamples = loadExamples(
- glfmExampleSnapshotsDir,
- 'prosemirror_json.yml',
- );
+ const exampleStatuses = loadExamples(glfmExampleStatusYml);
+ const markdownExamples = loadExamples(markdownYml);
+ const expectedHtmlExamples = loadExamples(htmlYml);
+ const expectedProseMirrorJsonExamples = loadExamples(prosemirrorJsonYml);
beforeAll(async () => {
return renderHtmlAndJsonForAllExamples(markdownExamples).then((examples) => {
diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
index bd48b7fdd23..5df901e0f15 100644
--- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
+++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
@@ -1,87 +1,8 @@
import { DOMSerializer } from 'prosemirror-model';
-// TODO: DRY up duplication with spec/frontend/content_editor/services/markdown_serializer_spec.js
-// See https://gitlab.com/groups/gitlab-org/-/epics/7719#plan
-import Audio from '~/content_editor/extensions/audio';
-import Blockquote from '~/content_editor/extensions/blockquote';
-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 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 Emoji from '~/content_editor/extensions/emoji';
-import Figure from '~/content_editor/extensions/figure';
-import FigureCaption from '~/content_editor/extensions/figure_caption';
-import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
-import FootnoteReference from '~/content_editor/extensions/footnote_reference';
-import FootnotesSection from '~/content_editor/extensions/footnotes_section';
-import Frontmatter from '~/content_editor/extensions/frontmatter';
-import HardBreak from '~/content_editor/extensions/hard_break';
-import Heading from '~/content_editor/extensions/heading';
-import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
-import HTMLNodes from '~/content_editor/extensions/html_nodes';
-import Image from '~/content_editor/extensions/image';
-import InlineDiff from '~/content_editor/extensions/inline_diff';
-import Italic from '~/content_editor/extensions/italic';
-import Link from '~/content_editor/extensions/link';
-import ListItem from '~/content_editor/extensions/list_item';
-import OrderedList from '~/content_editor/extensions/ordered_list';
-import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
-import Strike from '~/content_editor/extensions/strike';
-import Table from '~/content_editor/extensions/table';
-import TableCell from '~/content_editor/extensions/table_cell';
-import TableHeader from '~/content_editor/extensions/table_header';
-import TableRow from '~/content_editor/extensions/table_row';
-import TableOfContents from '~/content_editor/extensions/table_of_contents';
-import TaskItem from '~/content_editor/extensions/task_item';
-import TaskList from '~/content_editor/extensions/task_list';
-import Video from '~/content_editor/extensions/video';
import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
-import { createTestEditor } from 'jest/content_editor/test_utils';
+import { createTiptapEditor } from 'jest/content_editor/test_utils';
-const tiptapEditor = createTestEditor({
- extensions: [
- Audio,
- Blockquote,
- Bold,
- BulletList,
- Code,
- CodeBlockHighlight,
- DescriptionItem,
- DescriptionList,
- Details,
- DetailsContent,
- Emoji,
- FootnoteDefinition,
- FootnoteReference,
- FootnotesSection,
- Frontmatter,
- Figure,
- FigureCaption,
- HardBreak,
- Heading,
- HorizontalRule,
- ...HTMLNodes,
- Image,
- InlineDiff,
- Italic,
- Link,
- ListItem,
- OrderedList,
- ReferenceDefinition,
- Strike,
- Table,
- TableCell,
- TableHeader,
- TableRow,
- TableOfContents,
- TaskItem,
- TaskList,
- Video,
- ],
-});
+const tiptapEditor = createTiptapEditor();
export const IMPLEMENTATION_ERROR_MSG = 'Error - check implementation';
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 32193d97fd8..1bf23415052 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -1,4 +1,3 @@
-import Audio from '~/content_editor/extensions/audio';
import Blockquote from '~/content_editor/extensions/blockquote';
import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
@@ -16,7 +15,7 @@ import FootnoteReference from '~/content_editor/extensions/footnote_reference';
import HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
-import HTMLMarks from '~/content_editor/extensions/html_marks';
+import Highlight from '~/content_editor/extensions/highlight';
import HTMLNodes from '~/content_editor/extensions/html_nodes';
import Image from '~/content_editor/extensions/image';
import InlineDiff from '~/content_editor/extensions/inline_diff';
@@ -34,53 +33,13 @@ import TableHeader from '~/content_editor/extensions/table_header';
import TableRow from '~/content_editor/extensions/table_row';
import TaskItem from '~/content_editor/extensions/task_item';
import TaskList from '~/content_editor/extensions/task_list';
-import Video from '~/content_editor/extensions/video';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTiptapEditor, createDocBuilder } from '../test_utils';
jest.mock('~/emoji');
-const tiptapEditor = createTestEditor({
- extensions: [
- Audio,
- Blockquote,
- Bold,
- BulletList,
- Code,
- CodeBlockHighlight,
- DescriptionItem,
- DescriptionList,
- Details,
- DetailsContent,
- Emoji,
- FootnoteDefinition,
- FootnoteReference,
- Figure,
- FigureCaption,
- HardBreak,
- Heading,
- HorizontalRule,
- Image,
- InlineDiff,
- Italic,
- Link,
- ListItem,
- OrderedList,
- ReferenceDefinition,
- Sourcemap,
- Strike,
- Table,
- TableCell,
- TableHeader,
- TableRow,
- TaskItem,
- TaskList,
- Video,
- ...HTMLMarks,
- ...HTMLNodes,
- ],
-});
+const tiptapEditor = createTiptapEditor([Sourcemap]);
const {
builders: {
@@ -103,6 +62,7 @@ const {
figureCaption,
heading,
hardBreak,
+ highlight,
horizontalRule,
image,
inlineDiff,
@@ -141,6 +101,7 @@ const {
hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name },
+ highlight: { markType: Highlight.name },
image: { nodeType: Image.name },
inlineDiff: { markType: InlineDiff.name },
italic: { nodeType: Italic.name },
@@ -202,6 +163,12 @@ describe('markdownSerializer', () => {
).toBe('{++30 lines+}{--10 lines-}');
});
+ it('correctly serializes highlight', () => {
+ expect(serialize(paragraph('this is some ', highlight('highlighted'), ' text'))).toBe(
+ 'this is some <mark>highlighted</mark> text',
+ );
+ });
+
it('correctly serializes a line break', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 4ed1ed97cbd..0768fa6e8df 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -5,6 +5,45 @@ import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { builders, eq } from 'prosemirror-test-builder';
import { nextTick } from 'vue';
+import Audio from '~/content_editor/extensions/audio';
+import Blockquote from '~/content_editor/extensions/blockquote';
+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 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 Emoji from '~/content_editor/extensions/emoji';
+import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
+import FootnoteReference from '~/content_editor/extensions/footnote_reference';
+import FootnotesSection from '~/content_editor/extensions/footnotes_section';
+import Frontmatter from '~/content_editor/extensions/frontmatter';
+import Figure from '~/content_editor/extensions/figure';
+import FigureCaption from '~/content_editor/extensions/figure_caption';
+import HardBreak from '~/content_editor/extensions/hard_break';
+import Heading from '~/content_editor/extensions/heading';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import Highlight from '~/content_editor/extensions/highlight';
+import Image from '~/content_editor/extensions/image';
+import InlineDiff from '~/content_editor/extensions/inline_diff';
+import Italic from '~/content_editor/extensions/italic';
+import Link from '~/content_editor/extensions/link';
+import ListItem from '~/content_editor/extensions/list_item';
+import OrderedList from '~/content_editor/extensions/ordered_list';
+import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
+import Strike from '~/content_editor/extensions/strike';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableHeader from '~/content_editor/extensions/table_header';
+import TableRow from '~/content_editor/extensions/table_row';
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
+import TaskItem from '~/content_editor/extensions/task_item';
+import TaskList from '~/content_editor/extensions/task_list';
+import Video from '~/content_editor/extensions/video';
+import HTMLMarks from '~/content_editor/extensions/html_marks';
+import HTMLNodes from '~/content_editor/extensions/html_nodes';
export const createDocBuilder = ({ tiptapEditor, names = {} }) => {
const docBuilders = builders(tiptapEditor.schema, {
@@ -162,3 +201,49 @@ export const waitUntilNextDocTransaction = ({ tiptapEditor, action = () => {} })
action();
});
};
+
+export const createTiptapEditor = (extensions = []) =>
+ createTestEditor({
+ extensions: [
+ Audio,
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ DescriptionItem,
+ DescriptionList,
+ Details,
+ DetailsContent,
+ Emoji,
+ FootnoteDefinition,
+ FootnoteReference,
+ FootnotesSection,
+ Frontmatter,
+ Figure,
+ FigureCaption,
+ HardBreak,
+ Heading,
+ HorizontalRule,
+ ...HTMLMarks,
+ ...HTMLNodes,
+ Highlight,
+ Image,
+ InlineDiff,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ ReferenceDefinition,
+ Strike,
+ Table,
+ TableCell,
+ TableHeader,
+ TableRow,
+ TableOfContents,
+ TaskItem,
+ TaskList,
+ Video,
+ ...extensions,
+ ],
+ });
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 19e9ba8b268..990f18d64c1 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -59,6 +59,19 @@ describe('New Deploy Token', () => {
expect(checkbox.text()).toBe('read_registry');
});
+ function submitTokenThenCheck() {
+ wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
+
+ return waitForPromises()
+ .then(() => nextTick())
+ .then(() => {
+ const [tokenUsername, tokenValue] = wrapper.findAllComponents(GlFormInputGroup).wrappers;
+
+ expect(tokenUsername.props('value')).toBe('test token username');
+ expect(tokenValue.props('value')).toBe('test token');
+ });
+ }
+
it('should make a request to create a token on submit', () => {
const mockAxios = new MockAdapter(axios);
@@ -72,9 +85,18 @@ describe('New Deploy Token', () => {
const datepicker = wrapper.findAllComponents(GlDatepicker).at(0);
datepicker.vm.$emit('input', date);
- const [readRepo, readRegistry] = wrapper.findAllComponents(GlFormCheckbox).wrappers;
+ 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);
mockAxios
.onPost(createNewTokenPath, {
@@ -84,20 +106,47 @@ describe('New Deploy Token', () => {
username: 'test username',
read_repository: true,
read_registry: true,
+ write_registry: true,
+ read_package_registry: true,
+ write_package_registry: true,
},
})
.replyOnce(200, { username: 'test token username', token: 'test token' });
- wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
+ return submitTokenThenCheck();
+ });
- return waitForPromises()
- .then(() => nextTick())
- .then(() => {
- const [tokenUsername, tokenValue] = wrapper.findAllComponents(GlFormInputGroup).wrappers;
+ it('should request a token without an expiration date', () => {
+ const mockAxios = new MockAdapter(axios);
- expect(tokenUsername.props('value')).toBe('test token username');
- expect(tokenValue.props('value')).toBe('test token');
- });
+ const formInputs = wrapper.findAllComponents(GlFormInput);
+ const name = formInputs.at(0);
+ const username = formInputs.at(2);
+ name.vm.$emit('input', 'test never expire name');
+ username.vm.$emit('input', 'test never expire username');
+
+ const [, , , readPackageRegistry, writePackageRegistry] = wrapper.findAllComponents(
+ GlFormCheckbox,
+ ).wrappers;
+ readPackageRegistry.vm.$emit('input', true);
+ writePackageRegistry.vm.$emit('input', true);
+
+ mockAxios
+ .onPost(createNewTokenPath, {
+ deploy_token: {
+ name: 'test never expire name',
+ expires_at: null,
+ username: 'test never expire username',
+ read_repository: false,
+ read_registry: false,
+ write_registry: false,
+ read_package_registry: true,
+ write_package_registry: true,
+ },
+ })
+ .replyOnce(200, { username: 'test token username', token: 'test token' });
+
+ return submitTokenThenCheck();
});
});
});
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 4a070395eaf..439c20e0fb5 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -193,16 +193,18 @@ describe('deprecatedJQueryDropdown', () => {
});
it('should not focus search input while remote task is not complete', () => {
- expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).not.toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
it('should focus search input after remote task is complete', () => {
remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
it('should focus on input when opening for the second time after transition', () => {
@@ -215,7 +217,8 @@ describe('deprecatedJQueryDropdown', () => {
test.dropdownButtonElement.click();
test.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
});
@@ -225,7 +228,8 @@ describe('deprecatedJQueryDropdown', () => {
test.dropdownButtonElement.click();
test.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
});
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index 8cfe11c9040..ef1ed9bee51 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Design management index page designs renders error 1`] = `
<div
- class="gl-mt-5"
+ class="gl-mt-4"
data-testid="designs-root"
>
<!---->
@@ -34,7 +34,7 @@ exports[`Design management index page designs renders error 1`] = `
exports[`Design management index page designs renders loading icon 1`] = `
<div
- class="gl-mt-5"
+ class="gl-mt-4"
data-testid="designs-root"
>
<!---->
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index 8b25691ce34..a6f508c73eb 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -9,6 +9,18 @@ import {
const LINE_CODE = 'abc123';
+function problemsClone({
+ brokenSymlink = false,
+ brokenLineCode = false,
+ fileOnlyMoved = false,
+} = {}) {
+ return {
+ brokenSymlink,
+ brokenLineCode,
+ fileOnlyMoved,
+ };
+}
+
describe('isHighlighted', () => {
it('should return true if line is highlighted', () => {
const line = { line_code: LINE_CODE };
@@ -137,9 +149,12 @@ describe('classNameMapCell', () => {
describe('addCommentTooltip', () => {
const brokenSymLinkTooltip =
- 'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
+ 'Commenting on symbolic links that replace or are replaced by files is not supported';
const brokenRealTooltip =
- 'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
+ 'Commenting on files that replace or are replaced by symbolic links is not supported';
+ const lineMovedOrRenamedFileTooltip =
+ 'Commenting on files that are only moved or renamed is not supported';
+ const lineWithNoLineCodeTooltip = 'Commenting on this line is not supported';
const dragTooltip = 'Add a comment to this line or drag for multiple lines';
it('should return default tooltip', () => {
@@ -147,24 +162,38 @@ describe('addCommentTooltip', () => {
});
it('should return drag comment tooltip when dragging is enabled', () => {
- expect(utils.addCommentTooltip({})).toEqual(dragTooltip);
+ expect(utils.addCommentTooltip({ problems: problemsClone() })).toEqual(dragTooltip);
});
it('should return broken symlink tooltip', () => {
- expect(utils.addCommentTooltip({ commentsDisabled: { wasSymbolic: true } })).toEqual(
- brokenSymLinkTooltip,
- );
- expect(utils.addCommentTooltip({ commentsDisabled: { isSymbolic: true } })).toEqual(
- brokenSymLinkTooltip,
- );
+ expect(
+ utils.addCommentTooltip({
+ problems: problemsClone({ brokenSymlink: { wasSymbolic: true } }),
+ }),
+ ).toEqual(brokenSymLinkTooltip);
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isSymbolic: true } }) }),
+ ).toEqual(brokenSymLinkTooltip);
});
it('should return broken real tooltip', () => {
- expect(utils.addCommentTooltip({ commentsDisabled: { wasReal: true } })).toEqual(
- brokenRealTooltip,
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { wasReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ });
+
+ it('reports a tooltip when the line is in a file that has only been moved or renamed', () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone({ fileOnlyMoved: true }) })).toEqual(
+ lineMovedOrRenamedFileTooltip,
);
- expect(utils.addCommentTooltip({ commentsDisabled: { isReal: true } })).toEqual(
- brokenRealTooltip,
+ });
+
+ it("reports a tooltip when the line doesn't have a line code to leave a comment on", () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone({ brokenLineCode: true }) })).toEqual(
+ lineWithNoLineCodeTooltip,
);
});
});
@@ -211,6 +240,7 @@ describe('mapParallel', () => {
discussions: [{}],
discussionsExpanded: true,
hasForm: true,
+ problems: problemsClone(),
};
const content = {
diffFile: {},
diff --git a/spec/frontend/diffs/mock_data/diff_file.js b/spec/frontend/diffs/mock_data/diff_file.js
index dd200b0248c..e0e5778e0d5 100644
--- a/spec/frontend/diffs/mock_data/diff_file.js
+++ b/spec/frontend/diffs/mock_data/diff_file.js
@@ -1,3 +1,11 @@
+function problemsClone() {
+ return {
+ brokenSymlink: false,
+ brokenLineCode: false,
+ fileOnlyMoved: false,
+ };
+}
+
export const getDiffFileMock = () => ({
submodule: false,
submodule_link: null,
@@ -61,6 +69,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
@@ -71,6 +80,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
@@ -81,6 +91,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
@@ -91,6 +102,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
@@ -101,6 +113,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_6',
@@ -111,6 +124,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_7',
@@ -121,6 +135,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_9',
@@ -131,6 +146,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: null,
@@ -144,6 +160,7 @@ export const getDiffFileMock = () => ({
old_pos: 3,
new_pos: 5,
},
+ problems: problemsClone(),
},
],
parallel_diff_lines: [
@@ -158,6 +175,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -171,6 +189,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -183,6 +202,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
@@ -193,6 +213,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -205,6 +226,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
@@ -215,6 +237,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -227,6 +250,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
@@ -237,6 +261,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -249,6 +274,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_7',
@@ -259,6 +285,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -272,6 +299,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -287,6 +315,7 @@ export const getDiffFileMock = () => ({
old_pos: 3,
new_pos: 5,
},
+ problems: problemsClone(),
},
right: {
line_code: null,
@@ -300,6 +329,7 @@ export const getDiffFileMock = () => ({
old_pos: 3,
new_pos: 5,
},
+ problems: problemsClone(),
},
},
],
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index bf75f956d7f..87366cdbfc5 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -183,11 +183,11 @@ describe('DiffsStoreActions', () => {
beforeEach(() => {
delete noFilesData.diff_files;
-
- mock.onGet(endpointMetadata).reply(200, diffMetadata);
});
it('should fetch diff meta information', () => {
+ mock.onGet(endpointMetadata).reply(200, diffMetadata);
+
return testAction(
diffActions.fetchDiffFilesMeta,
{},
@@ -206,6 +206,40 @@ describe('DiffsStoreActions', () => {
[],
);
});
+
+ it('should show a warning on 404 reponse', async () => {
+ mock.onGet(endpointMetadata).reply(404);
+
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.stringMatching(
+ 'Building your merge request. Wait a few moments, then refresh this page.',
+ ),
+ variant: 'warning',
+ });
+ });
+
+ it('should show no warning on any other status code', async () => {
+ mock.onGet(endpointMetadata).reply(500);
+
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
describe('fetchCoverageFiles', () => {
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 3f870a98396..b5c44b084d8 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -311,9 +311,14 @@ describe('DiffsStoreUtils', () => {
describe('prepareLineForRenamedFile', () => {
const diffFile = {
file_hash: 'file-hash',
+ brokenSymlink: false,
+ renamed_file: false,
+ added_lines: 1,
+ removed_lines: 1,
};
const lineIndex = 4;
const sourceLine = {
+ line_code: 'abc',
foo: 'test',
rich_text: ' <p>rich</p>', // Note the leading space
};
@@ -328,6 +333,12 @@ describe('DiffsStoreUtils', () => {
hasForm: false,
text: undefined,
alreadyPrepared: true,
+ commentsDisabled: false,
+ problems: {
+ brokenLineCode: false,
+ brokenSymlink: false,
+ fileOnlyMoved: false,
+ },
};
let preppedLine;
@@ -360,24 +371,35 @@ describe('DiffsStoreUtils', () => {
});
it.each`
- brokenSymlink
- ${false}
- ${{}}
- ${'anything except `false`'}
+ brokenSymlink | renamed | added | removed | lineCode | commentsDisabled
+ ${false} | ${false} | ${0} | ${0} | ${'a'} | ${false}
+ ${{}} | ${false} | ${1} | ${1} | ${'a'} | ${true}
+ ${'truthy'} | ${false} | ${1} | ${1} | ${'a'} | ${true}
+ ${false} | ${true} | ${1} | ${1} | ${'a'} | ${false}
+ ${false} | ${true} | ${1} | ${0} | ${'a'} | ${false}
+ ${false} | ${true} | ${0} | ${1} | ${'a'} | ${false}
+ ${false} | ${true} | ${0} | ${0} | ${'a'} | ${true}
`(
- "properly assigns each line's `commentsDisabled` as the same value as the parent file's `brokenSymlink` value (`$brokenSymlink`)",
- ({ brokenSymlink }) => {
- preppedLine = utils.prepareLineForRenamedFile({
- diffViewType: INLINE_DIFF_VIEW_TYPE,
- line: sourceLine,
+ "properly sets a line's `commentsDisabled` to '$commentsDisabled' for file and line settings { brokenSymlink: $brokenSymlink, renamed: $renamed, added: $added, removed: $removed, line_code: $lineCode }",
+ ({ brokenSymlink, renamed, added, removed, lineCode, commentsDisabled }) => {
+ const line = {
+ ...sourceLine,
+ line_code: lineCode,
+ };
+ const file = {
+ ...diffFile,
+ brokenSymlink,
+ renamed_file: renamed,
+ added_lines: added,
+ removed_lines: removed,
+ };
+ const preparedLine = utils.prepareLineForRenamedFile({
index: lineIndex,
- diffFile: {
- ...diffFile,
- brokenSymlink,
- },
+ diffFile: file,
+ line,
});
- expect(preppedLine.commentsDisabled).toStrictEqual(brokenSymlink);
+ expect(preparedLine.commentsDisabled).toBe(commentsDisabled);
},
);
});
@@ -477,7 +499,7 @@ describe('DiffsStoreUtils', () => {
it('adds the `.brokenSymlink` property to each diff file', () => {
preparedDiff.diff_files.forEach((file) => {
- expect(file).toEqual(expect.objectContaining({ brokenSymlink: false }));
+ expect(file).toHaveProperty('brokenSymlink', false);
});
});
@@ -490,7 +512,7 @@ describe('DiffsStoreUtils', () => {
].flatMap((file) => [...file[INLINE_DIFF_LINES_KEY]]);
lines.forEach((line) => {
- expect(line.commentsDisabled).toBe(false);
+ expect(line.problems.brokenSymlink).toBe(false);
});
});
});
diff --git a/spec/frontend/diffs/utils/tree_worker_utils_spec.js b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
index 8113428f712..4df5fe75004 100644
--- a/spec/frontend/diffs/utils/tree_worker_utils_spec.js
+++ b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
@@ -35,6 +35,23 @@ describe('~/diffs/utils/tree_worker_utils', () => {
file_hash: 'test',
},
{
+ new_path: 'constructor/test/aFile.js',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 42,
+ file_hash: 'test',
+ },
+ {
+ new_path: 'submodule @ abcdef123',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 1,
+ submodule: true,
+ file_hash: 'test',
+ },
+ {
new_path: 'package.json',
deleted_file: true,
new_file: false,
@@ -66,6 +83,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
path: 'app/index.js',
removedLines: 10,
tempFile: false,
+ submodule: undefined,
type: 'blob',
tree: [],
},
@@ -87,6 +105,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
path: 'app/test/index.js',
removedLines: 0,
tempFile: true,
+ submodule: undefined,
type: 'blob',
tree: [],
},
@@ -101,6 +120,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
path: 'app/test/filepathneedstruncating.js',
removedLines: 0,
tempFile: true,
+ submodule: undefined,
type: 'blob',
tree: [],
},
@@ -110,6 +130,45 @@ describe('~/diffs/utils/tree_worker_utils', () => {
opened: true,
},
{
+ key: 'constructor',
+ name: 'constructor/test',
+ opened: true,
+ path: 'constructor',
+ tree: [
+ {
+ addedLines: 42,
+ changed: true,
+ deleted: false,
+ fileHash: 'test',
+ key: 'constructor/test/aFile.js',
+ name: 'aFile.js',
+ parentPath: 'constructor/test/',
+ path: 'constructor/test/aFile.js',
+ removedLines: 0,
+ submodule: undefined,
+ tempFile: true,
+ tree: [],
+ type: 'blob',
+ },
+ ],
+ type: 'tree',
+ },
+ {
+ key: 'submodule @ abcdef123',
+ parentPath: '/',
+ path: 'submodule @ abcdef123',
+ name: 'submodule @ abcdef123',
+ type: 'blob',
+ changed: true,
+ tempFile: true,
+ submodule: true,
+ deleted: false,
+ fileHash: 'test',
+ addedLines: 1,
+ removedLines: 0,
+ tree: [],
+ },
+ {
key: 'package.json',
parentPath: '/',
path: 'package.json',
@@ -117,6 +176,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
type: 'blob',
changed: true,
tempFile: false,
+ submodule: undefined,
deleted: true,
fileHash: 'test',
addedLines: 0,
@@ -135,6 +195,10 @@ describe('~/diffs/utils/tree_worker_utils', () => {
'app/test',
'app/test/index.js',
'app/test/filepathneedstruncating.js',
+ 'constructor',
+ 'constructor/test',
+ 'constructor/test/aFile.js',
+ 'submodule @ abcdef123',
'package.json',
]);
});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index fc86907c144..32126a5fd9a 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -18,9 +18,7 @@ import VariablesJson from './json_tests/positive_tests/variables.json';
import DefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/default_no_additional_properties.json';
import InheritDefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/inherit_default_no_additional_properties.json';
import JobVariablesMustNotContainObjectsJson from './json_tests/negative_tests/job_variables_must_not_contain_objects.json';
-import ReleaseAssetsLinksEmptyJson from './json_tests/negative_tests/release_assets_links_empty.json';
-import ReleaseAssetsLinksInvalidLinkTypeJson from './json_tests/negative_tests/release_assets_links_invalid_link_type.json';
-import ReleaseAssetsLinksMissingJson from './json_tests/negative_tests/release_assets_links_missing.json';
+import ReleaseAssetsLinksJson from './json_tests/negative_tests/release_assets_links.json';
import RetryUnknownWhenJson from './json_tests/negative_tests/retry_unknown_when.json';
// YAML POSITIVE TEST
@@ -31,34 +29,22 @@ import IncludeYaml from './yaml_tests/positive_tests/include.yml';
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';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
-import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml';
+import CacheKeyNeative from './yaml_tests/negative_tests/cache.yml';
import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml';
-import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
-import VariablesNegativeYaml from './yaml_tests/negative_tests/variables.yml';
-
+import JobWhenNegativeYaml from './yaml_tests/negative_tests/job_when.yml';
import ProjectPathIncludeEmptyYaml from './yaml_tests/negative_tests/project_path/include/empty.yml';
import ProjectPathIncludeInvalidVariableYaml from './yaml_tests/negative_tests/project_path/include/invalid_variable.yml';
import ProjectPathIncludeLeadSlashYaml from './yaml_tests/negative_tests/project_path/include/leading_slash.yml';
import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/include/no_slash.yml';
import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml';
-import ProjectPathTriggerIncludeEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/include/empty.yml';
-import ProjectPathTriggerIncludeInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml';
-import ProjectPathTriggerIncludeLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml';
-import ProjectPathTriggerIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml';
-import ProjectPathTriggerIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml';
-import ProjectPathTriggerMinimalEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml';
-import ProjectPathTriggerMinimalInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml';
-import ProjectPathTriggerMinimalLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml';
-import ProjectPathTriggerMinimalNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml';
-import ProjectPathTriggerMinimalTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml';
-import ProjectPathTriggerProjectEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/project/empty.yml';
-import ProjectPathTriggerProjectInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml';
-import ProjectPathTriggerProjectLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml';
-import ProjectPathTriggerProjectNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml';
-import ProjectPathTriggerProjectTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml';
+import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
+import TriggerNegative from './yaml_tests/negative_tests/trigger.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';
const ajv = new Ajv({
strictTypes: false,
@@ -68,7 +54,7 @@ const ajv = new Ajv({
ajv.addKeyword('markdownDescription');
AjvFormats(ajv);
-const schema = ajv.compile(CiSchema);
+const ajvSchema = ajv.compile(CiSchema);
describe('positive tests', () => {
it.each(
@@ -90,12 +76,17 @@ describe('positive tests', () => {
CacheYaml,
FilterYaml,
IncludeYaml,
+ JobWhenYaml,
RulesYaml,
VariablesYaml,
ProjectPathYaml,
}),
)('schema validates %s', (_, input) => {
- expect(input).toValidateJsonSchema(schema);
+ // We construct a new "JSON" from each main key that is inside a
+ // file which allow us to make sure each blob is valid.
+ Object.keys(input).forEach((key) => {
+ expect({ [key]: input[key] }).toValidateJsonSchema(ajvSchema);
+ });
});
});
@@ -106,39 +97,29 @@ describe('negative tests', () => {
DefaultNoAdditionalPropertiesJson,
JobVariablesMustNotContainObjectsJson,
InheritDefaultNoAdditionalPropertiesJson,
- ReleaseAssetsLinksEmptyJson,
- ReleaseAssetsLinksInvalidLinkTypeJson,
- ReleaseAssetsLinksMissingJson,
+ ReleaseAssetsLinksJson,
RetryUnknownWhenJson,
// YAML
ArtifactsNegativeYaml,
- CacheNegativeYaml,
+ CacheKeyNeative,
IncludeNegativeYaml,
+ JobWhenNegativeYaml,
RulesNegativeYaml,
- VariablesNegativeYaml,
+ VariablesInvalidSyntaxDescYaml,
+ VariablesWrongSyntaxUsageExpand,
ProjectPathIncludeEmptyYaml,
ProjectPathIncludeInvalidVariableYaml,
ProjectPathIncludeLeadSlashYaml,
ProjectPathIncludeNoSlashYaml,
ProjectPathIncludeTailSlashYaml,
- ProjectPathTriggerIncludeEmptyYaml,
- ProjectPathTriggerIncludeInvalidVariableYaml,
- ProjectPathTriggerIncludeLeadSlashYaml,
- ProjectPathTriggerIncludeNoSlashYaml,
- ProjectPathTriggerIncludeTailSlashYaml,
- ProjectPathTriggerMinimalEmptyYaml,
- ProjectPathTriggerMinimalInvalidVariableYaml,
- ProjectPathTriggerMinimalLeadSlashYaml,
- ProjectPathTriggerMinimalNoSlashYaml,
- ProjectPathTriggerMinimalTailSlashYaml,
- ProjectPathTriggerProjectEmptyYaml,
- ProjectPathTriggerProjectInvalidVariableYaml,
- ProjectPathTriggerProjectLeadSlashYaml,
- ProjectPathTriggerProjectNoSlashYaml,
- ProjectPathTriggerProjectTailSlashYaml,
+ TriggerNegative,
}),
)('schema validates %s', (_, input) => {
- expect(input).not.toValidateJsonSchema(schema);
+ // We construct a new "JSON" from each main key that is inside a
+ // file which allow us to make sure each blob is invalid.
+ Object.keys(input).forEach((key) => {
+ expect({ [key]: input[key] }).not.toValidateJsonSchema(ajvSchema);
+ });
});
});
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
index 955c19ef1ab..d30bc4649ab 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
@@ -9,4 +9,4 @@
"name": "test"
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
index 7411e4c2434..1a31467f9ae 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
@@ -1,8 +1,10 @@
{
"karma": {
"inherit": {
- "default": ["secrets"]
+ "default": [
+ "secrets"
+ ]
},
"script": "karma"
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
index bfdbf26ee70..68dd57824ab 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
@@ -1,7 +1,9 @@
{
"gitlab-ci-variables-object": {
"stage": "test",
- "script": ["true"],
+ "script": [
+ "true"
+ ],
"variables": {
"DEPLOY_ENVIRONMENT": {
"value": "staging",
@@ -9,4 +11,4 @@
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links.json
index 048911aefa3..00b5b54c7e2 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links.json
@@ -1,4 +1,24 @@
{
+ "gitlab-ci-release-assets-links-missing": {
+ "script": "dostuff",
+ "stage": "deploy",
+ "release": {
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "assets": {}
+ }
+ },
+ "gitlab-ci-release-assets-links-empty": {
+ "script": "dostuff",
+ "stage": "deploy",
+ "release": {
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "assets": {
+ "links": []
+ }
+ }
+ },
"gitlab-ci-release-assets-links-invalid-link-type": {
"script": "dostuff",
"stage": "deploy",
@@ -21,4 +41,4 @@
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
deleted file mode 100644
index 84a1aa14698..00000000000
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "gitlab-ci-release-assets-links-empty": {
- "script": "dostuff",
- "stage": "deploy",
- "release": {
- "description": "Created using the release-cli $EXTRA_DESCRIPTION",
- "tag_name": "$CI_COMMIT_TAG",
- "assets": {
- "links": []
- }
- }
- }
-}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
deleted file mode 100644
index 6f0b5a3bff8..00000000000
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "gitlab-ci-release-assets-links-missing": {
- "script": "dostuff",
- "stage": "deploy",
- "release": {
- "description": "Created using the release-cli $EXTRA_DESCRIPTION",
- "tag_name": "$CI_COMMIT_TAG",
- "assets": {}
- }
- }
-}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
index 433504f52c6..2c53ce07109 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
@@ -6,4 +6,4 @@
"when": "gitlab-ci-retry-object-unknown-when"
}
}
-}
+} \ No newline at end of file
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 04020c06753..3979c9ae2ac 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
@@ -1,13 +1,62 @@
-stages:
- - prepare
+cache-key-files-not-an-array:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files: package.json
+ paths:
+ - vendor/ruby
+ - node_modules
+
+cache-key-prefix-array:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix:
+ - binaries-cache-$CI_JOB_NAME
+ paths:
+ - binaries/
+
+cache-key-with-.:
+ script: echo "This job uses a cache."
+ cache:
+ key: .
+ paths:
+ - binaries/
+
+cache-key-with-multiple-.:
+ stage: test
+ script: echo "This job uses a cache."
+ cache:
+ key: ..
+ paths:
+ - binaries/
+
+cache-key-with-/:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-ca/che
+ paths:
+ - binaries/
+
+cache-path-not-an-array:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-cache
+ paths: binaries/*.apk
+
+cache-untracked-string:
+ script: echo "This job uses a cache."
+ cache:
+ untracked: 'true'
-# invalid cache:when values
-when no integer:
- stage: prepare
+when_integer:
+ script: echo "This job uses a cache."
cache:
when: 0
-when must be a reserved word:
- stage: prepare
+when_not_reserved_keyword:
+ script: echo "This job uses a cache."
cache:
when: 'never'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
index 1e16bb55405..6afd8baa0e8 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
@@ -1,6 +1,3 @@
-stages:
- - prepare
-
# invalid trigger:include
trigger missing file property:
stage: prepare
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml
new file mode 100644
index 00000000000..d4e3911ff60
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml
@@ -0,0 +1,11 @@
+job_with_wrong_when:
+ script: exit 0
+ when: on_xyz
+
+job_with_boolean_when:
+ script: exit 0
+ when: true
+
+job_with_array_when:
+ script: exit 0
+ when: [on_success]
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml
deleted file mode 100644
index ee2bb3e8ace..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml
deleted file mode 100644
index 770305be0dc..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml
deleted file mode 100644
index 82fd77cf0d3..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml
deleted file mode 100644
index f4ea59c7945..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml
deleted file mode 100644
index a0195c03352..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml
deleted file mode 100644
index cad8dbbf430..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml
deleted file mode 100644
index 6ca37666d09..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml
deleted file mode 100644
index 9d7c6b44125..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml
deleted file mode 100644
index acd047477c8..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml
deleted file mode 100644
index 0fdd00da3de..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml
deleted file mode 100644
index 0aa2330cecb..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml
deleted file mode 100644
index 3c17ec62039..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml
deleted file mode 100644
index f9884603171..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml
deleted file mode 100644
index d89e09756eb..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml
deleted file mode 100644
index 3c39d6be4cb..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml
new file mode 100644
index 00000000000..73cc82f2f1c
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml
@@ -0,0 +1,64 @@
+trigger-minimal-empty:
+ trigger: ''
+
+trigger-minimal-invalid-variable:
+ trigger: 'slug#'
+
+trigger-minimal-leading-slash:
+ trigger: '/slug'
+
+trigger-minimal-no-slash:
+ trigger: 'slug'
+
+trigger-minimal-trailing-slash:
+ trigger: 'slug/'
+
+trigger-project-empty:
+ trigger:
+ project: ''
+
+trigger-project-invalid-variable:
+ trigger:
+ project: 'slug#'
+
+trigger-project-leading-slash:
+ trigger:
+ project: '/slug'
+
+trigger-project-no-slash:
+ trigger:
+ project: 'slug'
+
+trigger-project-trailing-slash:
+ trigger:
+ project: 'slug/'
+
+trigger-include-empty:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: ''
+
+trigger-include-invalid:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug#'
+
+trigger-include-leading-slash:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: '/slug'
+
+trigger-include-no-slash:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug'
+
+trigger-include-trailing-slash:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_syntax_desc.yml
index a7f23cf0d73..4916a6b354e 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_syntax_desc.yml
@@ -1,4 +1,3 @@
-# invalid variable (unknown keyword is used)
variables:
FOO:
value: BAR
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml
new file mode 100644
index 00000000000..62bebfa57e7
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml
@@ -0,0 +1,4 @@
+variables:
+ RAW_VAR:
+ value: Hello $FOO
+ expand: okay
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 d83e14fdc6a..75918cd2a1b 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
@@ -1,24 +1,124 @@
-stages:
- - prepare
-
# valid cache:when values
job1:
- stage: prepare
script:
- echo 'running job'
cache:
when: 'on_success'
job2:
- stage: prepare
script:
- echo 'running job'
cache:
when: 'on_failure'
job3:
- stage: prepare
script:
- echo 'running job'
cache:
when: 'always'
+
+# valid cache:paths
+cache-paths:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-cache
+ paths:
+ - binaries/*.apk
+ - .config
+
+# valid cache:key
+cache-key-string:
+ script: echo "This job uses a cache."
+ cache:
+ key: random-string
+ paths:
+ - binaries/
+
+cache-key-string-with-dots:
+ script: echo "This job uses a cache."
+ cache:
+ key: random-..string
+ paths:
+ - binaries/
+
+cache-key-string-beginning-with-dot:
+ script: echo "This job uses a cache."
+ cache:
+ key: .random-string
+ paths:
+ - binaries/
+
+cache-key-string-ending-with-dot:
+ script: echo "This job uses a cache."
+ cache:
+ key: random-string.
+ paths:
+ - binaries/
+
+cache-key-predefined-variable:
+ script: echo "This job uses a cache."
+ cache:
+ key: $CI_COMMIT_REF_SLUG
+ paths:
+ - binaries/
+
+cache-key-combination:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-cache-$CI_COMMIT_REF_SLUG
+ paths:
+ - binaries/
+
+# valid cache:key:files
+cache-key-files:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ - package.json
+ paths:
+ - vendor/ruby
+ - node_modules
+
+# valide cache:key:prefix
+cache-key-prefix-string:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: random-string
+ paths:
+ - binaries/
+
+cache-key-prefix-predefined-variable:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: $CI_JOB_NAME
+ paths:
+ - binaries/
+
+cache-key-prefix-combination:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: binaries-cache-$CI_JOB_NAME
+ paths:
+ - binaries/
+
+# valid cache:untracked
+cache-untracked-true:
+ script: test
+ cache:
+ untracked: true
+
+cache-untracked-false:
+ script: test
+ cache:
+ untracked: false
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml
new file mode 100644
index 00000000000..2a684a78f4e
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml
@@ -0,0 +1,10 @@
+job_with_no_when:
+ script: exit 0
+
+job_with_when_always:
+ script: exit 0
+ when: always
+
+job_with_when_on_failure:
+ script: exit 0
+ when: on_failure
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 ee71087a72e..53d020c432f 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
@@ -6,3 +6,13 @@ variables:
description: "A single value variable"
DEPLOY_ENVIRONMENT:
description: "A multi-value variable"
+ RAW_VAR:
+ value: "Hello $FOO"
+ expand: false
+
+rspec:
+ script: rspec
+ variables:
+ RAW_VAR2:
+ value: "Hello $DEPLOY_ENVIRONMENT"
+ expand: false \ No newline at end of file
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index 1ff351b6554..19ebe0e3cb7 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -81,9 +81,18 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
},
path: previewMarkdownPath,
actionShowPreviewCondition: expect.any(Object),
+ eventEmitter: expect.any(Object),
});
});
+ it('support external preview trigger via emitter event', () => {
+ expect(panelSpy).not.toHaveBeenCalled();
+
+ instance.markdownPreview.eventEmitter.fire();
+
+ expect(panelSpy).toHaveBeenCalled();
+ });
+
describe('onDidLayoutChange', () => {
const emitter = new Emitter();
let layoutSpy;
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 68895b194a1..48483152f7a 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -10,11 +10,7 @@ import actionMutation from '~/environments/graphql/mutations/action.mutation.gra
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import createMockApollo from 'helpers/mock_apollo_helper';
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
const scheduledJobAction = {
name: 'scheduled action',
diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index be61c6fcc90..5d36209f8a6 100644
--- a/spec/frontend/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -76,7 +76,7 @@ describe('Rollback Component', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: setEnvironmentToRollback,
- variables: { environment },
+ variables: { environment: { ...environment, isLastDeployment: true, retryUrl } },
});
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index d246641b94b..355b77b55c3 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -537,6 +537,7 @@ export const folder = {
export const resolvedEnvironment = {
id: 41,
+ retryUrl: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
globalId: 'gid://gitlab/Environment/41',
name: 'review/hello',
state: 'available',
diff --git a/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
index 212b9ffc8f9..c958f669f9a 100644
--- a/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
+++ b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
@@ -10,6 +10,9 @@ describe('AjaxFilter', () => {
dummyConfig = {
endpoint: 'dummy endpoint',
searchKey: 'dummy search key',
+ searchValueFunction() {
+ return 'test';
+ },
};
dummyList = {
data: [],
@@ -40,7 +43,7 @@ describe('AjaxFilter', () => {
it('calls onLoadingFinished after loading data', async () => {
ajaxSpy = (url) => {
- expect(url).toBe('dummy endpoint?dummy search key=');
+ expect(url).toBe('dummy endpoint?dummy%20search%20key=test');
return Promise.resolve(dummyData);
};
@@ -51,7 +54,7 @@ describe('AjaxFilter', () => {
it('does not call onLoadingFinished if Ajax call fails', async () => {
const dummyError = new Error('My dummy is sick! :-(');
ajaxSpy = (url) => {
- expect(url).toBe('dummy endpoint?dummy search key=');
+ expect(url).toBe('dummy endpoint?dummy%20search%20key=test');
return Promise.reject(dummyError);
};
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index 7d95c506e6c..fae1f4056fb 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -7,7 +7,7 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let_it_be(:admin) { create(:admin, name: 'root') }
- let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' ) }
+ 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
4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index 5acc1095d5c..b14f402a7b9 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -7,7 +7,7 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin, name: 'root') }
- let(:namespace) { create(:namespace, name: 'gitlab-test' ) }
+ 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') }
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index b3ce23c8cd7..34e99ec647c 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -8,7 +8,7 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
include AdminModeHelper
let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') }
before do
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index 54c5b83da3e..b7b75247a59 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index 6cda2f0f665..25626ed8c76 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Branches (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let_it_be(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb
index 426a76f29e0..ff15cfb62c3 100644
--- a/spec/frontend/fixtures/clusters.rb
+++ b/spec/frontend/fixtures/clusters.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace) }
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb
index 24d602216d8..05fca368fd5 100644
--- a/spec/frontend/fixtures/deploy_keys.rb
+++ b/spec/frontend/fixtures/deploy_keys.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :c
include AdminModeHelper
let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:project2) { create(:project, :internal) }
let(:project3) { create(:project, :internal) }
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
index dd16bd81b51..5aa466ef015 100644
--- a/spec/frontend/fixtures/freeze_period.rb
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
around do |example|
freeze_time do
# Mock time to sept 19 (intl. talk like a pirate day)
- Timecop.travel(2020, 9, 19)
+ travel_to(Time.utc(2020, 9, 19))
example.run
end
diff --git a/spec/frontend/fixtures/integrations.rb b/spec/frontend/fixtures/integrations.rb
index 45d1c400f5d..c26b9524324 100644
--- a/spec/frontend/fixtures/integrations.rb
+++ b/spec/frontend/fixtures/integrations.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') }
let!(:service) { create(:custom_issue_tracker_integration, project: project) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index e3d88098841..bc5ece20032 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
include JavaScriptFixturesHelpers
let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
render_views
diff --git a/spec/frontend/fixtures/job_artifacts.rb b/spec/frontend/fixtures/job_artifacts.rb
new file mode 100644
index 00000000000..e53cdbbaaa5
--- /dev/null
+++ b/spec/frontend/fixtures/job_artifacts.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Job Artifacts (GraphQL fixtures)' do
+ describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ job_artifacts_query_path = 'artifacts/graphql/queries/get_job_artifacts.query.graphql'
+
+ it "graphql/#{job_artifacts_query_path}.json" do
+ create(:ci_build, :failed, :artifacts, :trace_artifact, pipeline: pipeline)
+ create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline)
+
+ query = get_graphql_query_as_string(job_artifacts_query_path)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 3657a5405a4..ac58b99875b 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include GraphqlHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') }
let(:user) { project.first_owner }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb
index 2445c9376e2..9b8d073e74c 100644
--- a/spec/frontend/fixtures/labels.rb
+++ b/spec/frontend/fixtures/labels.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Labels (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
let(:user) { create(:user) }
- let(:group) { create(:group, name: 'frontend-fixtures-group' ) }
+ let(:group) { create(:group, name: 'frontend-fixtures-group') }
let(:project) { create(:project_empty_repo, namespace: group, path: 'labels-project') }
let!(:project_label_bug) { create(:label, project: project, title: 'bug', color: '#FF0000') }
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index cbf26a70e5f..18f89fbc5e5 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
let(:user) { project.first_owner }
@@ -147,6 +147,20 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
expect_graphql_errors_to_be_empty
end
end
+
+ context 'merge request in state getState query' do
+ base_input_path = 'vue_merge_request_widget/queries/'
+ base_output_path = 'graphql/merge_requests/'
+ query_name = 'get_state.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
private
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index ff4b27844a6..cd22d110e38 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
let(:user) { project.first_owner }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, description: '- [ ] Task List Item') }
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
index 7f8b3d378d3..109b016d980 100644
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ b/spec/frontend/fixtures/metrics_dashboard.rb
@@ -7,7 +7,7 @@ RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:namespace) { create(:namespace, name: 'monitoring' ) }
+ let_it_be(:namespace) { create(:namespace, name: 'monitoring') }
let_it_be(:project) { project_with_dashboard_namespace('.gitlab/dashboards/test.yml', nil, namespace: namespace) }
let_it_be(:environment) { create(:environment, id: 1, project: project) }
let_it_be(:params) { { environment: environment } }
diff --git a/spec/frontend/fixtures/namespaces.rb b/spec/frontend/fixtures/namespaces.rb
index a3f295f4e66..9858e3241cb 100644
--- a/spec/frontend/fixtures/namespaces.rb
+++ b/spec/frontend/fixtures/namespaces.rb
@@ -32,6 +32,26 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
end
end
+ describe API::Groups, type: :request do
+ let_it_be(:user) { create(:user) }
+
+ describe 'transfer_locations' do
+ let_it_be(:groups) { create_list(:group, 4) }
+ let_it_be(:transfer_from_group) { create(:group) }
+
+ before_all do
+ groups.each { |group| group.add_owner(user) }
+ transfer_from_group.add_owner(user)
+ end
+
+ it 'api/groups/transfer_locations.json' do
+ get api("/groups/#{transfer_from_group.id}/transfer_locations", user)
+
+ expect(response).to be_successful
+ end
+ end
+ end
+
describe GraphQL::Query, type: :request do
let_it_be(:user) { create(:user) }
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 4de0bd762f8..3bfe9113e83 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include GraphqlHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
@@ -54,7 +54,7 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
get_pipeline_schedules_query = 'get_pipeline_schedules.query.graphql'
let_it_be(:query) do
- get_graphql_query_as_string("pipeline_schedules/graphql/queries/#{get_pipeline_schedules_query}")
+ get_graphql_query_as_string("ci/pipeline_schedules/graphql/queries/#{get_pipeline_schedules_query}")
end
it "#{fixtures_path}#{get_pipeline_schedules_query}.json" do
@@ -71,5 +71,14 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
+
+ it "#{fixtures_path}#{get_pipeline_schedules_query}.take_ownership.json" do
+ maintainer = create(:user)
+ project.add_maintainer(maintainer)
+
+ post_graphql(query, current_user: maintainer, variables: { projectPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
end
end
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 114db26d6a9..44b471a70d8 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') }
let_it_be(:commit_without_author) { RepoHelpers.another_sample_commit }
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index b9c427c7505..101ba203a57 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
runners_token = 'runnerstoken:intabulasreferre'
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) }
diff --git a/spec/frontend/fixtures/prometheus_integration.rb b/spec/frontend/fixtures/prometheus_integration.rb
index 250c50bc8bb..13130c00118 100644
--- a/spec/frontend/fixtures/prometheus_integration.rb
+++ b/spec/frontend/fixtures/prometheus_integration.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') }
let!(:integration) { create(:prometheus_integration, project: project) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb
index 7bd5b8c5f6c..886f5525ac5 100644
--- a/spec/frontend/fixtures/raw.rb
+++ b/spec/frontend/fixtures/raw.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Raw files', '(JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') }
let(:response) { @response }
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index b523650dda5..de87114766e 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -20,8 +20,8 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:build) { create(:ci_build, runner: runner) }
- query_path = 'runner/graphql/'
- fixtures_path = 'graphql/runner/'
+ query_path = 'ci/runner/graphql/'
+ fixtures_path = 'graphql/ci/runner/'
after(:all) do
remove_repository(project)
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 58d4bc5c1f3..0510746a944 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:user) { project.first_owner }
let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) }
diff --git a/spec/frontend/fixtures/static/gl_field_errors.html b/spec/frontend/fixtures/static/gl_field_errors.html
index f8470e02b7c..a53366fc29f 100644
--- a/spec/frontend/fixtures/static/gl_field_errors.html
+++ b/spec/frontend/fixtures/static/gl_field_errors.html
@@ -17,6 +17,9 @@
<div class="form-group">
<input class="custom gl-field-error-ignore" type="text">Custom, do not validate</input>
</div>
+<div class="form-group">
+<textarea required title="Textarea is required">Textarea</textarea>
+</div>
<div class="form-group"></div>
<input class="submit" type="submit">Submit</input>
</form>
diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb
index d934396f803..58f230de546 100644
--- a/spec/frontend/fixtures/todos.rb
+++ b/spec/frontend/fixtures/todos.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Todos (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:user) { project.first_owner }
let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index a809bf248bf..a105b0b165c 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -193,6 +193,20 @@ describe('Flash', () => {
);
});
+ describe('with title', () => {
+ const mockTitle = 'my title';
+
+ it('shows title and message', () => {
+ createAlert({
+ title: mockTitle,
+ message: mockMessage,
+ });
+
+ const text = document.querySelector('.flash-container').textContent.trim();
+ expect(text).toBe(`${mockTitle} ${mockMessage}`);
+ });
+ });
+
describe('with buttons', () => {
const findAlertAction = () => document.querySelector('.flash-container .gl-alert-action');
diff --git a/spec/frontend/gfm_auto_complete/mock_data.js b/spec/frontend/gfm_auto_complete/mock_data.js
index 86795ffd0a5..9c5a9d7ef3d 100644
--- a/spec/frontend/gfm_auto_complete/mock_data.js
+++ b/spec/frontend/gfm_auto_complete/mock_data.js
@@ -32,3 +32,60 @@ export const eventlistenersMockDefaultMap = [
namespace: 'atwho',
},
];
+
+export const crmContactsMock = [
+ {
+ id: 1,
+ email: 'contact.1@email.com',
+ firstName: 'Contact',
+ lastName: 'One',
+ search: 'contact.1@email.com',
+ state: 'active',
+ set: false,
+ },
+ {
+ id: 2,
+ email: 'contact.2@email.com',
+ firstName: 'Contact',
+ lastName: 'Two',
+ search: 'contact.2@email.com',
+ state: 'active',
+ set: false,
+ },
+ {
+ id: 3,
+ email: 'contact.3@email.com',
+ firstName: 'Contact',
+ lastName: 'Three',
+ search: 'contact.3@email.com',
+ state: 'inactive',
+ set: false,
+ },
+ {
+ id: 4,
+ email: 'contact.4@email.com',
+ firstName: 'Contact',
+ lastName: 'Four',
+ search: 'contact.4@email.com',
+ state: 'inactive',
+ set: true,
+ },
+ {
+ id: 5,
+ email: 'contact.5@email.com',
+ firstName: 'Contact',
+ lastName: 'Five',
+ search: 'contact.5@email.com',
+ state: 'active',
+ set: true,
+ },
+ {
+ id: 5,
+ email: 'contact.6@email.com',
+ firstName: 'Contact',
+ lastName: 'Six',
+ search: 'contact.6@email.com',
+ state: 'active',
+ set: undefined, // On purpose
+ },
+];
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index c3dfc4570f9..68225f39c66 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -3,14 +3,23 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete';
+import GfmAutoComplete, {
+ membersBeforeSave,
+ highlighter,
+ CONTACT_STATE_ACTIVE,
+ CONTACTS_ADD_COMMAND,
+ CONTACTS_REMOVE_COMMAND,
+} from 'ee_else_ce/gfm_auto_complete';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
-import { eventlistenersMockDefaultMap } from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
+import {
+ eventlistenersMockDefaultMap,
+ crmContactsMock,
+} from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
describe('GfmAutoComplete', () => {
const fetchDataMock = { fetchData: jest.fn() };
@@ -871,7 +880,87 @@ describe('GfmAutoComplete', () => {
});
});
- describe('Contacts', () => {
+ describe('CRM Contacts', () => {
+ const dataSources = {
+ contacts: `${TEST_HOST}/autocomplete_sources/contacts`,
+ };
+
+ const allContacts = crmContactsMock;
+ const assignedContacts = allContacts.filter((contact) => contact.set);
+ const unassignedContacts = allContacts.filter(
+ (contact) => contact.state === CONTACT_STATE_ACTIVE && !contact.set,
+ );
+
+ let autocomplete;
+ let $textarea;
+
+ beforeEach(() => {
+ setHTMLFixture('<textarea></textarea>');
+ autocomplete = new GfmAutoComplete(dataSources);
+ $textarea = $('textarea');
+ autocomplete.setup($textarea, { contacts: true });
+ });
+
+ afterEach(() => {
+ autocomplete.destroy();
+ resetHTMLFixture();
+ });
+
+ const triggerDropdown = (text) => {
+ $textarea.trigger('focus').val(text).caret('pos', -1);
+ $textarea.trigger('keyup');
+
+ jest.runOnlyPendingTimers();
+ };
+
+ const getDropdownItems = () => {
+ const dropdown = document.getElementById('at-view-contacts');
+ const items = dropdown.getElementsByTagName('li');
+ return [].map.call(items, (item) => item.textContent.trim());
+ };
+
+ const expectContacts = ({ input, output }) => {
+ triggerDropdown(input);
+
+ expect(getDropdownItems()).toEqual(output.map((contact) => contact.email));
+ };
+
+ describe('with no contacts assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['[contact:'] = [...unassignedContacts];
+ });
+
+ it.each`
+ input | output
+ ${`${CONTACTS_ADD_COMMAND} [contact:`} | ${unassignedContacts}
+ ${`${CONTACTS_REMOVE_COMMAND} [contact:`} | ${[]}
+ `('$input shows $output.length contacts', expectContacts);
+ });
+
+ describe('with some contacts assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['[contact:'] = allContacts;
+ });
+
+ it.each`
+ input | output
+ ${`${CONTACTS_ADD_COMMAND} [contact:`} | ${unassignedContacts}
+ ${`${CONTACTS_REMOVE_COMMAND} [contact:`} | ${assignedContacts}
+ `('$input shows $output.length contacts', expectContacts);
+ });
+
+ describe('with all contacts assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['[contact:'] = [...assignedContacts];
+ });
+
+ it.each`
+ input | output
+ ${`${CONTACTS_ADD_COMMAND} [contact:`} | ${[]}
+ ${`${CONTACTS_REMOVE_COMMAND} [contact:`} | ${assignedContacts}
+ `('$input shows $output.length contacts', expectContacts);
+ });
+
it('escapes name and email correct', () => {
const xssPayload = '<script>alert(1)</script>';
const escapedPayload = '&lt;script&gt;alert(1)&lt;/script&gt;';
diff --git a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
new file mode 100644
index 00000000000..949bcf71ff5
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
@@ -0,0 +1,102 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import GitlabVersionCheckBadge from '~/gitlab_version_check/components/gitlab_version_check_badge.vue';
+import { STATUS_TYPES, UPGRADE_DOCS_URL } from '~/gitlab_version_check/constants';
+
+describe('GitlabVersionCheckBadge', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const defaultProps = {
+ status: STATUS_TYPES.SUCCESS,
+ };
+
+ const createComponent = (props = {}) => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+
+ wrapper = shallowMountExtended(GitlabVersionCheckBadge, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ unmockTracking();
+ wrapper.destroy();
+ });
+
+ const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+
+ describe('template', () => {
+ describe.each`
+ status | expectedUI
+ ${STATUS_TYPES.SUCCESS} | ${{ title: 'Up to date', variant: 'success' }}
+ ${STATUS_TYPES.WARNING} | ${{ title: 'Update available', variant: 'warning' }}
+ ${STATUS_TYPES.DANGER} | ${{ title: 'Update ASAP', variant: 'danger' }}
+ `('badge ui', ({ status, expectedUI }) => {
+ beforeEach(() => {
+ createComponent({ status, actionable: true });
+ });
+
+ describe(`when status is ${status}`, () => {
+ it(`title is ${expectedUI.title}`, () => {
+ expect(findGlBadge().text()).toBe(expectedUI.title);
+ });
+
+ it(`variant is ${expectedUI.variant}`, () => {
+ expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
+ });
+
+ it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
+ label: 'version_badge',
+ property: expectedUI.title,
+ });
+ });
+
+ it(`link is ${UPGRADE_DOCS_URL}`, () => {
+ expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
+ await findGlBadgeClickWrapper().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'version_badge',
+ property: expectedUI.title,
+ });
+ });
+ });
+ });
+
+ describe('when actionable is false', () => {
+ beforeEach(() => {
+ createComponent({ actionable: false });
+ });
+
+ it('tracks rendered_version_badge correctly', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
+ label: 'version_badge',
+ property: 'Up to date',
+ });
+ });
+
+ it('does not provide a link to GlBadge', () => {
+ expect(findGlBadge().attributes('href')).toBe(undefined);
+ });
+
+ it('does not track click_version_badge', async () => {
+ await findGlBadgeClickWrapper().trigger('click');
+
+ expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'version_badge',
+ property: 'Up to date',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gitlab_version_check/index_spec.js b/spec/frontend/gitlab_version_check/index_spec.js
new file mode 100644
index 00000000000..8a11ff48bf2
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/index_spec.js
@@ -0,0 +1,116 @@
+import Vue from 'vue';
+import * as Sentry from '@sentry/browser';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import initGitlabVersionCheck from '~/gitlab_version_check';
+
+describe('initGitlabVersionCheck', () => {
+ let originalGon;
+ let mock;
+ let vueApps;
+
+ 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();
+ };
+
+ 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);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gl_field_errors_spec.js b/spec/frontend/gl_field_errors_spec.js
index 92d04927ee5..1f6929baa75 100644
--- a/spec/frontend/gl_field_errors_spec.js
+++ b/spec/frontend/gl_field_errors_spec.js
@@ -27,7 +27,7 @@ describe('GL Style Field Errors', () => {
expect(testContext.fieldErrors).toBeDefined();
const { inputs } = testContext.fieldErrors.state;
- expect(inputs.length).toBe(4);
+ expect(inputs.length).toBe(5);
});
it('should ignore elements with custom error handling', () => {
diff --git a/spec/frontend/google_cloud/service_accounts/list_spec.js b/spec/frontend/google_cloud/service_accounts/list_spec.js
index 7a76a893757..c2bd2005b5d 100644
--- a/spec/frontend/google_cloud/service_accounts/list_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/list_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlAlert, GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { GlAlert, GlButton, GlEmptyState, GlLink, GlTable } from '@gitlab/ui';
import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue';
describe('google_cloud/service_accounts/list', () => {
@@ -45,7 +45,26 @@ describe('google_cloud/service_accounts/list', () => {
beforeEach(() => {
const propsData = {
- list: [{}, {}, {}],
+ list: [
+ {
+ ref: '*',
+ gcp_project: 'gcp-project-123',
+ service_account_exists: true,
+ service_account_key_exists: true,
+ },
+ {
+ ref: 'prod',
+ gcp_project: 'gcp-project-456',
+ service_account_exists: true,
+ service_account_key_exists: true,
+ },
+ {
+ ref: 'stag',
+ gcp_project: 'gcp-project-789',
+ service_account_exists: true,
+ service_account_key_exists: true,
+ },
+ ],
createUrl: '#create-url',
emptyIllustrationUrl: '#empty-illustration-url',
};
@@ -68,6 +87,12 @@ describe('google_cloud/service_accounts/list', () => {
expect(findRows().length).toBe(4);
});
+ it('table row must contain link to the google cloud console', () => {
+ expect(findRows().at(1).findComponent(GlLink).attributes('href')).toBe(
+ `${ServiceAccountsList.GOOGLE_CONSOLE_URL}?project=gcp-project-123`,
+ );
+ });
+
it('shows the link to create new service accounts', () => {
const button = findButton();
expect(button.exists()).toBe(true);
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index 93e087e10f2..b615679dcc5 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -67,6 +67,7 @@ describe('OverviewTabs', () => {
const findTabPanels = () => wrapper.findAllComponents(GlTab);
const findTab = (name) => wrapper.findByRole('tab', { name });
const findSelectedTab = () => wrapper.findByRole('tab', { selected: true });
+ const findSearchInput = () => wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder);
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
@@ -244,18 +245,39 @@ describe('OverviewTabs', () => {
};
describe('when search is typed in', () => {
- const search = 'Foo bar';
+ describe('when search is greater than or equal to 3 characters', () => {
+ const search = 'Foo bar';
- beforeEach(async () => {
- await setup();
- await wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).setValue(search);
- });
+ beforeEach(async () => {
+ await setup();
+ await findSearchInput().setValue(search);
+ });
- it('updates query string with `filter` key', () => {
- expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } });
+ it('updates query string with `filter` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } });
+ });
+
+ sharedAssertions({ search, sort: defaultProvide.initialSort });
});
- sharedAssertions({ search, sort: defaultProvide.initialSort });
+ describe('when search is less than 3 characters', () => {
+ const search = 'Fo';
+
+ beforeEach(async () => {
+ await setup();
+ await findSearchInput().setValue(search);
+ });
+
+ it('does not emit `fetchFilteredAndSortedGroups` event from `eventHub`', () => {
+ expect(eventHub.$emit).not.toHaveBeenCalledWith(
+ `${ACTIVE_TAB_SUBGROUPS_AND_PROJECTS}fetchFilteredAndSortedGroups`,
+ {
+ filterGroupsBy: search,
+ sortBy: defaultProvide.initialSort,
+ },
+ );
+ });
+ });
});
describe('when sort is changed', () => {
@@ -308,6 +330,16 @@ describe('OverviewTabs', () => {
).toBe('Foo bar');
});
+ describe('when search is cleared', () => {
+ it('removes `filter` key from query string', async () => {
+ await findSearchInput().setValue('');
+
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { sort: SORTING_ITEM_UPDATED.desc },
+ });
+ });
+ });
+
it('sets sort dropdown', () => {
expect(wrapper.findComponent(GlSorting).props()).toMatchObject({
text: SORTING_ITEM_UPDATED.label,
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
index 7cbe6e5bbab..0065820f78f 100644
--- a/spec/frontend/groups/components/transfer_group_form_spec.js
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -1,8 +1,13 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import Component from '~/groups/components/transfer_group_form.vue';
+import TransferLocationsForm, { i18n } from '~/groups/components/transfer_group_form.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import { getGroupTransferLocations } from '~/api/groups_api';
+
+jest.mock('~/api/groups_api', () => ({
+ getGroupTransferLocations: jest.fn(),
+}));
describe('Transfer group form', () => {
let wrapper;
@@ -22,25 +27,25 @@ describe('Transfer group form', () => {
];
const defaultProps = {
- groupNamespaces,
paidGroupHelpLink,
isPaidGroup: false,
confirmationPhrase,
confirmButtonText,
};
- const createComponent = (propsData = {}) =>
- shallowMountExtended(Component, {
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMountExtended(TransferLocationsForm, {
propsData: {
...defaultProps,
...propsData,
},
stubs: { GlSprintf },
});
+ };
const findAlert = () => wrapper.findComponent(GlAlert);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
+ const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findHiddenInput = () => wrapper.find('[name="new_parent_group_id"]');
afterEach(() => {
@@ -49,21 +54,17 @@ describe('Transfer group form', () => {
describe('default', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
- it('renders the namespace select component', () => {
- expect(findNamespaceSelect().exists()).toBe(true);
- });
+ it('renders the transfer locations dropdown and passes correct props', () => {
+ findTransferLocations().props('groupTransferLocationsApiMethod')();
- it('sets the namespace select properties', () => {
- expect(findNamespaceSelect().props()).toMatchObject({
- defaultText: 'Select parent group',
- fullWidth: false,
- includeHeaders: false,
- emptyNamespaceTitle: 'No parent group',
- includeEmptyNamespace: true,
- groupNamespaces,
+ expect(getGroupTransferLocations).toHaveBeenCalled();
+ expect(findTransferLocations().props()).toMatchObject({
+ value: null,
+ label: i18n.dropdownLabel,
+ additionalDropdownItems: TransferLocationsForm.additionalDropdownItems,
});
});
@@ -90,10 +91,15 @@ describe('Transfer group form', () => {
});
describe('with a selected project', () => {
- const [firstGroup] = groupNamespaces;
+ const [selectedItem] = groupNamespaces;
+
beforeEach(() => {
- wrapper = createComponent();
- findNamespaceSelect().vm.$emit('select', firstGroup);
+ createComponent();
+ findTransferLocations().vm.$emit('input', selectedItem);
+ });
+
+ it('sets `value` prop on `TransferLocations` component', () => {
+ expect(findTransferLocations().props('value')).toEqual(selectedItem);
});
it('sets the confirm danger disabled property to false', () => {
@@ -102,7 +108,7 @@ describe('Transfer group form', () => {
it('sets the hidden input field', () => {
expect(findHiddenInput().exists()).toBe(true);
- expect(parseInt(findHiddenInput().attributes('value'), 10)).toBe(firstGroup.id);
+ expect(findHiddenInput().attributes('value')).toBe(String(selectedItem.id));
});
it('emits "confirm" event when the danger modal is confirmed', () => {
@@ -116,15 +122,15 @@ describe('Transfer group form', () => {
describe('isPaidGroup = true', () => {
beforeEach(() => {
- wrapper = createComponent({ isPaidGroup: true });
+ createComponent({ isPaidGroup: true });
});
it('disables the transfer button', () => {
expect(findConfirmDanger().props()).toMatchObject({ disabled: true });
});
- it('hides the namespace selector button', () => {
- expect(findNamespaceSelect().exists()).toBe(false);
+ it('hides the transfer locations dropdown', () => {
+ expect(findTransferLocations().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js
new file mode 100644
index 00000000000..74424ee3230
--- /dev/null
+++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js
@@ -0,0 +1,377 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlAlert,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
+import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
+import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { __ } from '~/locale';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import { getTransferLocations } from '~/api/projects_api';
+import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
+
+jest.mock('~/api/projects_api', () => ({
+ getTransferLocations: jest.fn(),
+}));
+
+describe('TransferLocations', () => {
+ let wrapper;
+
+ // Default data
+ const resourceId = '1';
+ const defaultPropsData = {
+ groupTransferLocationsApiMethod: getTransferLocations,
+ value: null,
+ };
+ const additionalDropdownItem = {
+ id: -1,
+ humanName: __('No parent group'),
+ };
+
+ // Mock requests
+ const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
+ const mockResolvedGetTransferLocations = ({
+ data = transferLocationsResponsePage1,
+ page = '1',
+ nextPage = '2',
+ total = '4',
+ totalPages = '2',
+ prevPage = null,
+ } = {}) => {
+ getTransferLocations.mockResolvedValueOnce({
+ data,
+ headers: {
+ 'x-per-page': '2',
+ 'x-page': page,
+ 'x-total': total,
+ 'x-total-pages': totalPages,
+ 'x-next-page': nextPage,
+ 'x-prev-page': prevPage,
+ },
+ });
+ };
+ const mockRejectedGetTransferLocations = () => {
+ const error = new Error();
+
+ getTransferLocations.mockRejectedValueOnce(error);
+ };
+
+ // VTU wrapper helpers
+ Vue.use(VueApollo);
+ const createComponent = ({
+ propsData = {},
+ requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
+ } = {}) => {
+ wrapper = mountExtended(TransferLocations, {
+ provide: {
+ resourceId,
+ },
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ apolloProvider: createMockApollo(requestHandlers),
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+ };
+ const findUserTransferLocations = () =>
+ wrapper
+ .findByTestId('user-transfer-locations')
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((dropdownItem) => dropdownItem.text());
+ const findGroupTransferLocations = () =>
+ wrapper
+ .findByTestId('group-transfer-locations')
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((dropdownItem) => dropdownItem.text());
+ const findDropdownItemByText = (text) =>
+ wrapper
+ .findAllComponents(GlDropdownItem)
+ .wrappers.find((dropdownItem) => dropdownItem.text() === text);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
+ const searchEmitInput = (searchTerm = 'foo') => findSearch().vm.$emit('input', searchTerm);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when `GlDropdown` is opened', () => {
+ it('shows loading icon', async () => {
+ getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
+ createComponent();
+ findDropdown().vm.$emit('show');
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('fetches and renders user and group transfer locations', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
+
+ expect(findUserTransferLocations()).toEqual([namespace.fullName]);
+ expect(findGroupTransferLocations()).toEqual(
+ transferLocationsResponsePage1.map((transferLocation) => transferLocation.full_name),
+ );
+ });
+
+ describe('when `showUserTransferLocations` prop is `false`', () => {
+ it('does not fetch user transfer locations', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent({
+ propsData: {
+ showUserTransferLocations: false,
+ },
+ });
+ await showDropdown();
+
+ expect(wrapper.findByTestId('user-transfer-locations').exists()).toBe(false);
+ });
+ });
+
+ describe('when `additionalDropdownItems` prop is passed', () => {
+ it('displays additional dropdown items', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ });
+ await showDropdown();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true);
+ });
+
+ describe('when loading', () => {
+ it('does not display additional dropdown items', async () => {
+ getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
+ createComponent({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ });
+ findDropdown().vm.$emit('show');
+ await nextTick();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined();
+ });
+ });
+ });
+
+ describe('when transfer locations have already been fetched', () => {
+ beforeEach(async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+ });
+
+ it('does not fetch transfer locations', async () => {
+ getTransferLocations.mockClear();
+ defaultQueryHandler.mockClear();
+
+ await showDropdown();
+
+ expect(getTransferLocations).not.toHaveBeenCalled();
+ expect(defaultQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays dismissible error alert', async () => {
+ mockRejectedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+
+ alert.vm.$emit('dismiss');
+ await nextTick();
+
+ expect(alert.exists()).toBe(false);
+ });
+ });
+
+ describe('when `currentUser` GraphQL query fails', () => {
+ it('displays error alert', async () => {
+ mockResolvedGetTransferLocations();
+ const error = new Error();
+ createComponent({
+ requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
+ });
+ await showDropdown();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when transfer location is selected', () => {
+ it('displays transfer location as selected', () => {
+ const [{ id, full_name: humanName }] = transferLocationsResponsePage1;
+
+ createComponent({
+ propsData: {
+ value: {
+ id,
+ humanName,
+ },
+ },
+ });
+
+ expect(findDropdown().props('text')).toBe(humanName);
+ });
+ });
+
+ describe('when search is typed in', () => {
+ const transferLocationsResponseSearch = [transferLocationsResponsePage1[0]];
+
+ const arrange = async ({ propsData, searchTerm } = {}) => {
+ mockResolvedGetTransferLocations();
+ createComponent({ propsData });
+ await showDropdown();
+ mockResolvedGetTransferLocations({ data: transferLocationsResponseSearch });
+ searchEmitInput(searchTerm);
+ await nextTick();
+ };
+
+ it('sets `isSearchLoading` prop to `true`', async () => {
+ await arrange();
+
+ expect(findSearch().props('isLoading')).toBe(true);
+ });
+
+ it('passes `search` param to API call and updates group transfer locations', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ resourceId,
+ expect.objectContaining({ search: 'foo' }),
+ );
+ expect(findGroupTransferLocations()).toEqual(
+ transferLocationsResponseSearch.map((transferLocation) => transferLocation.full_name),
+ );
+ });
+
+ it('does not display additional dropdown items if they do not match the search', async () => {
+ await arrange({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ });
+ await waitForPromises();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined();
+ });
+
+ it('displays additional dropdown items if they match the search', async () => {
+ await arrange({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ searchTerm: 'No par',
+ });
+ await waitForPromises();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true);
+ });
+ });
+
+ describe('when there are no more pages', () => {
+ it('does not show intersection observer', async () => {
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage1,
+ nextPage: null,
+ total: '2',
+ totalPages: '1',
+ prevPage: null,
+ });
+ createComponent();
+ await showDropdown();
+
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+ });
+
+ describe('when intersection observer appears', () => {
+ const arrange = async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage2,
+ page: '2',
+ nextPage: null,
+ prevPage: '1',
+ totalPages: '2',
+ });
+
+ intersectionObserverEmitAppear();
+ await nextTick();
+ };
+
+ it('shows loading icon', async () => {
+ await arrange();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('passes `page` param to API call', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ resourceId,
+ expect.objectContaining({ page: 2 }),
+ );
+ });
+
+ it('updates dropdown with new group transfer locations', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(findGroupTransferLocations()).toEqual(
+ [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
+ ({ full_name: fullName }) => fullName,
+ ),
+ );
+ });
+ });
+
+ describe('when `label` prop is passed', () => {
+ it('renders label', () => {
+ const label = 'Foo bar';
+
+ createComponent({ propsData: { label } });
+
+ expect(wrapper.findByRole('group', { name: label }).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 48c670757a2..a575f428a69 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -3,9 +3,11 @@ import Vue from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { stubPerformanceWebAPI } from 'helpers/performance';
+import { __ } from '~/locale';
import CannotPushCodeAlert from '~/ide/components/cannot_push_code_alert.vue';
import ErrorMessage from '~/ide/components/error_message.vue';
import Ide from '~/ide/components/ide.vue';
+import eventHub from '~/ide/eventhub';
import { MSG_CANNOT_PUSH_CODE_GO_TO_FORK, MSG_GO_TO_FORK } from '~/ide/messages';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
@@ -14,6 +16,7 @@ import { projectData } from '../mock_data';
Vue.use(Vuex);
const TEST_FORK_IDE_PATH = '/test/ide/path';
+const MSG_ARE_YOU_SURE = __('Are you sure you want to lose unsaved changes?');
describe('WebIDE', () => {
const emptyProjData = { ...projectData, empty_repo: true, branches: {} };
@@ -40,6 +43,8 @@ describe('WebIDE', () => {
const findAlert = () => wrapper.findComponent(CannotPushCodeAlert);
+ const callOnBeforeUnload = (e = {}) => window.onbeforeunload(e);
+
beforeEach(() => {
stubPerformanceWebAPI();
@@ -49,6 +54,7 @@ describe('WebIDE', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ window.onbeforeunload = null;
});
describe('ide component, empty repo', () => {
@@ -90,7 +96,8 @@ describe('WebIDE', () => {
describe('onBeforeUnload', () => {
it('returns undefined when no staged files or changed files', () => {
createComponent();
- expect(wrapper.vm.onBeforeUnload()).toBe(undefined);
+
+ expect(callOnBeforeUnload()).toBe(undefined);
});
it('returns warning text when their are changed files', () => {
@@ -100,7 +107,10 @@ describe('WebIDE', () => {
},
});
- expect(wrapper.vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
+ const e = {};
+
+ expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE);
+ expect(e.returnValue).toBe(MSG_ARE_YOU_SURE);
});
it('returns warning text when their are staged files', () => {
@@ -110,20 +120,27 @@ describe('WebIDE', () => {
},
});
- expect(wrapper.vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
+ const e = {};
+
+ expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE);
+ expect(e.returnValue).toBe(MSG_ARE_YOU_SURE);
});
- it('updates event object', () => {
- const event = {};
+ it('returns undefined once after "skip-beforeunload" was emitted', () => {
createComponent({
state: {
stagedFiles: [file()],
},
});
- wrapper.vm.onBeforeUnload(event);
+ eventHub.$emit('skip-beforeunload');
+ const e = {};
+
+ expect(callOnBeforeUnload()).toBe(undefined);
+ expect(e.returnValue).toBe(undefined);
- expect(event.returnValue).toBe('Are you sure you want to lose unsaved changes?');
+ expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE);
+ expect(e.returnValue).toBe(MSG_ARE_YOU_SURE);
});
});
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index 1d38231a767..e92f843ae6e 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
@@ -127,5 +127,29 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
});
});
});
+
+ describe('with initOpenView that does not exist', () => {
+ beforeEach(async () => {
+ createComponent({ extensionTabs, initOpenView: 'does-not-exist' });
+
+ await nextTick();
+ });
+
+ it('nothing is dispatched', () => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with initOpenView that does exist', () => {
+ beforeEach(async () => {
+ createComponent({ extensionTabs, initOpenView: fakeView.name });
+
+ await nextTick();
+ });
+
+ it('dispatches open with view on create', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('rightPane/open', fakeView);
+ });
+ });
});
});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 4555f519bc2..b7349b8fed1 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -3,12 +3,16 @@ 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;
@@ -33,6 +37,19 @@ describe('ide/components/panes/right.vue', () => {
wrapper = null;
});
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders collapsible-sidebar', () => {
+ expect(wrapper.findComponent(CollapsibleSidebar).props()).toMatchObject({
+ side: 'right',
+ initOpenView: SWITCH_EDITORS_VIEW_NAME,
+ });
+ });
+ });
+
describe('pipelines tab', () => {
it('is always shown', () => {
createComponent();
@@ -113,4 +130,32 @@ 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/switch_editors/switch_editors_view_spec.js b/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
new file mode 100644
index 00000000000..7a958391fea
--- /dev/null
+++ b/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
@@ -0,0 +1,214 @@
+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/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 4117f2648bd..ae21d257bb2 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -87,11 +87,13 @@ describe('Multi-file store mutations', () => {
emptyStateSvgPath: 'emptyState',
noChangesStateSvgPath: 'noChanges',
committedStateSvgPath: 'committed',
+ switchEditorSvgPath: 'switchEditorSvg',
});
expect(localState.emptyStateSvgPath).toBe('emptyState');
expect(localState.noChangesStateSvgPath).toBe('noChanges');
expect(localState.committedStateSvgPath).toBe('committed');
+ expect(localState.switchEditorSvgPath).toBe('switchEditorSvg');
});
});
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 a0115cb9349..61f860688dc 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -1,10 +1,11 @@
-import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createAlert } from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
@@ -50,6 +51,7 @@ describe('import table', () => {
rowWrapper.find('[data-testid="target-namespace-selector"]');
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
+ const findNewPathCol = () => wrapper.find('[data-test-id="new-path-col"]');
const triggerSelectAllCheckbox = (checked = true) =>
wrapper.find('thead input[type=checkbox]').setChecked(checked);
@@ -76,6 +78,9 @@ describe('import table', () => {
historyPath: '/fake_history_path',
defaultTargetNamespace,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
apolloProvider,
});
};
@@ -540,6 +545,26 @@ describe('import table', () => {
);
});
+ it('displays info icon with a tooltip', async () => {
+ const NEW_GROUPS = [generateFakeEntry({ id: 1, status: STATUSES.NONE })];
+
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: NEW_GROUPS,
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ await waitForPromises();
+
+ const icon = findNewPathCol().findComponent(GlIcon);
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe('Path of the new group.');
+ });
+
describe('unavailable features warning', () => {
it('renders alert when there are unavailable features', async () => {
createComponent({
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 0a3beee0507..7e67379f5ab 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlForm } from '@gitlab/ui';
+import { GlAlert, GlBadge, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser';
@@ -80,6 +80,7 @@ describe('IntegrationForm', () => {
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);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
@@ -715,45 +716,72 @@ describe('IntegrationForm', () => {
});
});
- describe('Help and sections rendering', () => {
- const dummyHelp = 'Foo Help';
+ 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.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}
+ prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert
+ ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true}
+ ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${true} | ${false}
+ ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${false} | ${false}
+ ${'does not'} | ${'foo'} | ${true} | ${true} | ${false}
+ ${'does not'} | ${'foo'} | ${false} | ${true} | ${false}
+ ${'does not'} | ${'foo'} | ${true} | ${false} | ${false}
`(
- '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
- ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ '$prefix render the upgrade warnning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack"',
+ ({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => {
createComponent({
provide: {
- helpHtml,
glFeatures: { integrationSlackAppNotifications: flagIsOn },
},
customStateProps: {
- sections,
+ shouldUpgradeSlack,
type: integration,
+ sections: [mockSectionConnection],
},
});
- expect(findAllSections().length > 0).toEqual(shouldShowSections);
- expect(findHelpHtml().exists()).toBe(shouldShowHelp);
- if (shouldShowHelp) {
- expect(findHelpHtml().html()).toContain(helpHtml);
- }
+ expect(findAlert().exists()).toBe(shouldShowAlert);
},
);
});
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index c329ca8522f..082eeea30f1 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -1,5 +1,6 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { placeholderForType } from 'jh_else_ce/integrations/constants';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
@@ -28,6 +29,50 @@ describe('TriggerFields', () => {
const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
const findAllGlFormInputs = () => wrapper.findAllComponents(GlFormInput);
+ describe('placeholder text on the event fields and default values', () => {
+ const dummyFieldPlaceholder = '#foo';
+ const integrationTypes = {
+ INTEGRATION_TYPE_SLACK: 'slack',
+ INTEGRATION_TYPE_SLACK_APPLICATION: 'gitlab_slack_application',
+ INTEGRATION_TYPE_MATTERMOST: 'mattermost',
+ INTEGRATION_TYPE_NON_EXISTING: 'non_existing',
+ };
+ it.each`
+ integrationType | fieldPlaceholder | expectedPlaceholder
+ ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_MATTERMOST]}
+ ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_MATTERMOST]}
+ ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${undefined} | ${undefined}
+ ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${''} | ${undefined}
+ ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ `(
+ 'passed down correct placeholder for "$integrationType" type and "$fieldPlaceholder" placeholder on the field',
+ ({ integrationType, fieldPlaceholder, expectedPlaceholder }) => {
+ createComponent({
+ type: integrationType,
+ events: [
+ {
+ field: {
+ name: 'foo',
+ value: '',
+ placeholder: fieldPlaceholder,
+ },
+ },
+ ],
+ });
+ const field = wrapper.findComponent(GlFormInput);
+
+ expect(field.attributes('placeholder')).toBe(expectedPlaceholder);
+ },
+ );
+ });
+
describe.each([true, false])('template, isInheriting = `%p`', (isInheriting) => {
it('renders a label with text "Trigger"', () => {
createComponent();
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 e9e1fbad07b..47be1933ed7 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -10,12 +10,12 @@ import InviteMembersModal from '~/invite_members/components/invite_members_modal
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
+import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
import {
INVITE_MEMBERS_FOR_TASK,
MEMBERS_MODAL_CELEBRATE_INTRO,
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_PLACEHOLDER,
- MEMBERS_PLACEHOLDER_DISABLED,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
EXPANDED_ERRORS,
@@ -31,8 +31,6 @@ import {
propsData,
inviteSource,
newProjectPath,
- freeUsersLimit,
- membersCount,
user1,
user2,
user3,
@@ -99,6 +97,7 @@ describe('InviteMembersModal', () => {
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
+ const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
const findAccordion = () => wrapper.findComponent(GlCollapse);
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
const findMemberErrorMessage = (element) =>
@@ -112,7 +111,7 @@ describe('InviteMembersModal', () => {
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
- const membersFormGroupText = () => findMembersFormGroup().text();
+ const membersFormGroupDescription = () => findMembersFormGroup().attributes('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
@@ -299,19 +298,8 @@ describe('InviteMembersModal', () => {
describe('members form group description', () => {
it('renders correct description', () => {
- createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
- });
-
- describe('when reached user limit', () => {
- it('renders correct description', () => {
- createInviteMembersToProjectWrapper(
- { freeUsersLimit, membersCount: 5 },
- { GlFormGroup },
- );
-
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
- });
+ createInviteMembersToProjectWrapper({ GlFormGroup });
+ expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
});
@@ -339,23 +327,10 @@ describe('InviteMembersModal', () => {
describe('members form group description', () => {
it('renders correct description', async () => {
- createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
+ createInviteMembersToProjectWrapper({ GlFormGroup });
await triggerOpenModal({ mode: 'celebrate' });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
- });
-
- describe('when reached user limit', () => {
- it('renders correct description', async () => {
- createInviteMembersToProjectWrapper(
- { freeUsersLimit, membersCount: 5 },
- { GlFormGroup },
- );
-
- await triggerOpenModal({ mode: 'celebrate' });
-
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
- });
+ expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
});
@@ -370,20 +345,39 @@ describe('InviteMembersModal', () => {
describe('members form group description', () => {
it('renders correct description', () => {
- createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
- });
-
- describe('when reached user limit', () => {
- it('renders correct description', () => {
- createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount: 5 }, { GlFormGroup });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
- });
+ createInviteMembersToGroupWrapper({ GlFormGroup });
+ expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
});
});
+ describe('rendering the user limit notification', () => {
+ it('shows the user limit notification alert when reached limit', () => {
+ const usersLimitDataset = { reachedLimit: true };
+
+ createInviteMembersToProjectWrapper(usersLimitDataset);
+
+ expect(findUserLimitAlert().exists()).toBe(true);
+ });
+
+ it('shows the user limit notification alert when close to dashboard limit', () => {
+ const usersLimitDataset = { closeToDashboardLimit: true };
+
+ createInviteMembersToProjectWrapper(usersLimitDataset);
+
+ expect(findUserLimitAlert().exists()).toBe(true);
+ });
+
+ it('does not show the user limit notification alert', () => {
+ const usersLimitDataset = {};
+
+ createInviteMembersToProjectWrapper(usersLimitDataset);
+
+ expect(findUserLimitAlert().exists()).toBe(false);
+ });
+ });
+
describe('submitting the invite form', () => {
const mockInvitationsApi = (code, data) => {
mock.onPost(GROUPS_INVITATIONS_PATH).reply(code, data);
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 b55eeb72471..aeead8809fd 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -18,10 +18,7 @@ import {
CANCEL_BUTTON_TEXT,
INVITE_BUTTON_TEXT_DISABLED,
INVITE_BUTTON_TEXT,
- CANCEL_BUTTON_TEXT_DISABLED,
ON_SHOW_TRACK_LABEL,
- ON_CLOSE_TRACK_LABEL,
- ON_SUBMIT_TRACK_LABEL,
} from '~/invite_members/constants';
import { propsData, membersPath, purchasePath } from '../mock_data/modal_base';
@@ -131,7 +128,9 @@ describe('InviteModalBase', () => {
it('renders description', () => {
createComponent({}, { GlFormGroup });
- expect(findMembersFormGroup().text()).toContain(propsData.formGroupDescription);
+ expect(findMembersFormGroup().attributes('description')).toContain(
+ propsData.formGroupDescription,
+ );
});
describe('when users limit is reached', () => {
@@ -145,30 +144,13 @@ describe('InviteModalBase', () => {
beforeEach(() => {
createComponent(
- { usersLimitDataset: { membersPath, purchasePath }, reachedLimit: true },
+ { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } },
{ GlModal, GlFormGroup },
);
});
- it('renders correct blocks', () => {
- expect(findIcon().exists()).toBe(true);
- expect(findDisabledInput().exists()).toBe(true);
- expect(findDropdown().exists()).toBe(false);
- expect(findDatepicker().exists()).toBe(false);
- });
-
- it('renders correct buttons', () => {
- const cancelButton = findCancelButton();
- const actionButton = findActionButton();
-
- expect(cancelButton.attributes('href')).toBe(purchasePath);
- expect(cancelButton.text()).toBe(CANCEL_BUTTON_TEXT_DISABLED);
- expect(actionButton.attributes('href')).toBe(membersPath);
- expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED);
- });
-
it('tracks actions', () => {
- createComponent({ reachedLimit: true }, { GlFormGroup, GlModal });
+ createComponent({ usersLimitDataset: { reachedLimit: true } }, { GlFormGroup, GlModal });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
const modal = wrapper.findComponent(GlModal);
@@ -176,37 +158,20 @@ describe('InviteModalBase', () => {
modal.vm.$emit('shown');
expectTracking('render', ON_SHOW_TRACK_LABEL);
- modal.vm.$emit('cancel', { preventDefault: jest.fn() });
- expectTracking('click_button', ON_CLOSE_TRACK_LABEL);
-
- modal.vm.$emit('primary', { preventDefault: jest.fn() });
- expectTracking('click_button', ON_SUBMIT_TRACK_LABEL);
-
unmockTracking();
});
-
- describe('when free user namespace', () => {
- it('hides cancel button', () => {
- createComponent(
- {
- usersLimitDataset: { membersPath, purchasePath, userNamespace: true },
- reachedLimit: true,
- },
- { GlModal, GlFormGroup },
- );
-
- expect(findCancelButton().exists()).toBe(false);
- });
- });
});
describe('when user limit is close on a personal namespace', () => {
beforeEach(() => {
createComponent(
{
- closeToLimit: true,
- reachedLimit: false,
- usersLimitDataset: { membersPath, userNamespace: true },
+ usersLimitDataset: {
+ membersPath,
+ userNamespace: true,
+ closeToDashboardLimit: true,
+ reachedLimit: false,
+ },
},
{ GlModal, GlFormGroup },
);
diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js
index 1ff2e86412f..2a780490468 100644
--- a/spec/frontend/invite_members/components/user_limit_notification_spec.js
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -1,8 +1,8 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
-import { REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE } from '~/invite_members/constants';
-import { freeUsersLimit, membersCount } from '../mock_data/member_modal';
+import { REACHED_LIMIT_VARIANT, CLOSE_TO_LIMIT_VARIANT } from '~/invite_members/constants';
+import { freeUsersLimit, remainingSeats } from '../mock_data/member_modal';
const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name';
@@ -10,20 +10,16 @@ describe('UserLimitNotification', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findTrialLink = () => wrapper.findByTestId('trial-link');
+ const findUpgradeLink = () => wrapper.findByTestId('upgrade-link');
- const createComponent = (
- closeToLimit = false,
- reachedLimit = false,
- usersLimitDataset = {},
- props = {},
- ) => {
+ const createComponent = (limitVariant, usersLimitDataset = {}, props = {}) => {
wrapper = shallowMountExtended(UserLimitNotification, {
propsData: {
- closeToLimit,
- reachedLimit,
+ limitVariant,
usersLimitDataset: {
+ remainingSeats,
freeUsersLimit,
- membersCount,
newTrialRegistrationPath: 'newTrialRegistrationPath',
purchasePath: 'purchasePath',
...usersLimitDataset,
@@ -35,40 +31,46 @@ describe('UserLimitNotification', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when limit is not reached', () => {
- it('renders empty block', () => {
- createComponent();
-
- expect(findAlert().exists()).toBe(false);
- });
- });
-
describe('when close to limit within a group', () => {
it("renders user's limit notification", () => {
- createComponent(true, false, { membersCount: 3 });
+ createComponent(CLOSE_TO_LIMIT_VARIANT);
const alert = findAlert();
expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE);
- expect(alert.text()).toEqual(
- 'To get more members an owner of the group can start a trial or upgrade to a paid tier.',
- );
+ expect(alert.text()).toContain('To get more members an owner of the group can');
});
});
describe('when limit is reached', () => {
it("renders user's limit notification", () => {
- createComponent(true, true);
+ createComponent(REACHED_LIMIT_VARIANT);
const alert = findAlert();
expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name");
- expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE);
+ expect(alert.text()).toContain(
+ 'To invite new users to this namespace, you must remove existing users.',
+ );
});
});
+
+ describe('tracking', () => {
+ it.each([CLOSE_TO_LIMIT_VARIANT, REACHED_LIMIT_VARIANT])(
+ `has tracking attributes for %j variant`,
+ (variant) => {
+ createComponent(variant);
+
+ expect(findTrialLink().attributes('data-track-action')).toBe('click_link');
+ expect(findTrialLink().attributes('data-track-label')).toBe(
+ `start_trial_user_limit_notification_${variant}`,
+ );
+ expect(findUpgradeLink().attributes('data-track-action')).toBe('click_link');
+ expect(findUpgradeLink().attributes('data-track-label')).toBe(
+ `upgrade_user_limit_notification_${variant}`,
+ );
+ },
+ );
+ });
});
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 4f4e9345e46..59d58f21bb0 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -19,7 +19,7 @@ export const propsData = {
export const inviteSource = 'unknown';
export const newProjectPath = 'projects/new';
export const freeUsersLimit = 5;
-export const membersCount = 1;
+export const remainingSeats = 2;
export const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
export const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
new file mode 100644
index 00000000000..c432d722637
--- /dev/null
+++ b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
@@ -0,0 +1,554 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { cloneDeep } from 'lodash';
+import VueApollo from 'vue-apollo';
+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 { 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 issuableEventHub from '~/issues/list/eventhub';
+import moveIssueMutation from '~/issuable/bulk_update_sidebar/components/graphql/mutations/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';
+import {
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+} from '~/work_items/constants';
+
+jest.mock('~/flash');
+jest.mock('~/lib/logger');
+useMockLocationHelper();
+
+const mockDefaultProps = {
+ projectFullPath: 'flight/FlightJS',
+ projectsFetchPath: '/-/autocomplete/projects?project_id=1',
+};
+
+const mockDestinationProject = {
+ full_path: 'gitlab-org/GitLabTest',
+};
+
+const mockMutationErrorMessage = 'Example error message';
+
+const mockIssue = {
+ iid: '15',
+ type: WORK_ITEM_TYPE_ENUM_ISSUE,
+};
+
+const mockIncident = {
+ iid: '32',
+ type: WORK_ITEM_TYPE_ENUM_INCIDENT,
+};
+
+const mockTask = {
+ iid: '40',
+ type: WORK_ITEM_TYPE_ENUM_TASK,
+};
+
+const mockTestCase = {
+ iid: '51',
+ type: WORK_ITEM_TYPE_ENUM_TEST_CASE,
+};
+
+const selectedIssuesMocks = {
+ tasksOnly: [mockTask],
+ testCasesOnly: [mockTestCase],
+ issuesOnly: [mockIssue, mockIncident],
+ tasksAndTestCases: [mockTask, mockTestCase],
+ issuesAndTasks: [mockIssue, mockIncident, mockTask],
+ issuesAndTestCases: [mockIssue, mockIncident, mockTestCase],
+ issuesTasksAndTestCases: [mockIssue, mockIncident, mockTask, mockTestCase],
+};
+
+let getIssuesQueryCompleteResponse = getIssuesQueryResponse;
+if (IS_EE) {
+ getIssuesQueryCompleteResponse = cloneDeep(getIssuesQueryResponse);
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].blockingCount = 1;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].healthStatus = null;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].weight = 5;
+}
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [],
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [{ message: mockMutationErrorMessage }],
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue({});
+
+const mockIssuesQueryResponse = jest.fn().mockResolvedValue(getIssuesQueryCompleteResponse);
+const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
+
+describe('MoveIssuesButton', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDropdown = () => wrapper.findComponent(IssuableMoveDropdown);
+ const emitMoveIssuablesEvent = () => {
+ findDropdown().vm.$emit('move-issuable', mockDestinationProject);
+ };
+
+ const createComponent = (data = {}, mutationResolverMock = rejectedMutationMock) => {
+ fakeApollo = createMockApollo([
+ [moveIssueMutation, mutationResolverMock],
+ [getIssuesQuery, mockIssuesQueryResponse],
+ [getIssuesCountsQuery, mockIssuesCountsQueryResponse],
+ ]);
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesQuery,
+ variables: {
+ isProject: true,
+ fullPath: mockDefaultProps.projectFullPath,
+ },
+ data: getIssuesQueryCompleteResponse.data,
+ });
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesCountsQuery,
+ variables: {
+ isProject: true,
+ },
+ data: getIssuesCountsQueryResponse.data,
+ });
+
+ wrapper = shallowMount(MoveIssuesButton, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ ...mockDefaultProps,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ beforeEach(() => {
+ // Needed due to a bug in Apollo: https://github.com/apollographql/apollo-client/issues/8900
+ // eslint-disable-next-line no-console
+ console.warn = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ describe('`Move selected` dropdown', () => {
+ it('renders disabled by default', () => {
+ createComponent();
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ });
+
+ it.each`
+ selectedIssuablesMock | disabled | status | testMessage
+ ${[]} | ${true} | ${'disabled'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'disabled'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'disabled'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'enabled'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'disabled'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${false} | ${'enabled'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${false} | ${'enabled'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${false} | ${'enabled'} | ${'issues and tasks and test cases are selected'}
+ `('renders $status if $testMessage', async ({ selectedIssuablesMock, disabled }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ if (disabled) {
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ } else {
+ expect(findDropdown().attributes('disabled')).toBeUndefined();
+ }
+ });
+ });
+
+ describe('warning message', () => {
+ it.each`
+ selectedIssuablesMock | warningExists | visibility | message | testMessage
+ ${[]} | ${false} | ${'not visible'} | ${'empty'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'not visible'} | ${'empty'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'issues and tasks and test cases are selected'}
+ `(
+ 'is $visibility with `$message` message if $testMessage',
+ async ({ selectedIssuablesMock, warningExists, message }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ const alert = findAlert();
+ expect(alert.exists()).toBe(warningExists);
+
+ if (warningExists) {
+ expect(alert.text()).toBe(message);
+ expect(alert.attributes('variant')).toBe('warning');
+ }
+ },
+ );
+ });
+
+ describe('moveIssues method', () => {
+ describe('changes the `Move selected` dropdown loading state', () => {
+ it('keeps loading state to false when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to true when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(true);
+ });
+
+ it('sets loading state to false when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+ });
+
+ describe('handles events', () => {
+ beforeEach(() => {
+ jest.spyOn(issuableEventHub, '$emit');
+ });
+
+ it('does not emit any event when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('emits `issuables:bulkMoveStarted` when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveStarted');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+ });
+
+ describe('shows errors', () => {
+ it('does not create flashes or logs errors when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when issues are moved without errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('creates a flash and logs errors when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We're mocking two issues so it will log two errors
+ expect(logError).toHaveBeenCalledTimes(2);
+ expect(logError).toHaveBeenNthCalledWith(
+ 1,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+ expect(logError).toHaveBeenNthCalledWith(
+ 2,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+
+ // Only one flash is created even if multiple errors are reported
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+
+ it('creates a flash but not logs errors when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+ });
+
+ describe('calls mutations', () => {
+ it('does not call any mutation when no issue is selected', async () => {
+ createComponent({}, resolvedMutationWithoutErrorsMock);
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.testCasesOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks and test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('calls a mutation for every selected issue skipping tasks', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We mock three elements but only two are valid issues since the task is skipped
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenCalledTimes(2);
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(1, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[0].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(2, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[1].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 1b2935ce5d1..996b2406240 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -40,12 +40,12 @@ describe('RelatedIssuesBlock', () => {
});
it.each`
- issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText
- ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked items'} | ${'Read more about related issues'} | ${'Add a related issue'}
- ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'}
+ issuableType | pathIdSeparator | titleText | addButtonText
+ ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked items'} | ${'Add a related issue'}
+ ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Add a related epic'}
`(
- 'displays "$titleText" in the header, "$helpLinkText" aria-label for help link, and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
- ({ issuableType, pathIdSeparator, titleText, helpLinkText, addButtonText }) => {
+ 'displays "$titleText" in the header and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
+ ({ issuableType, pathIdSeparator, titleText, addButtonText }) => {
wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator,
@@ -56,9 +56,6 @@ describe('RelatedIssuesBlock', () => {
});
expect(wrapper.find('.card-title').text()).toContain(titleText);
- expect(wrapper.find('[data-testid="help-link"]').attributes('aria-label')).toBe(
- helpLinkText,
- );
expect(findIssueCountBadgeAddButton().attributes('aria-label')).toBe(addButtonText);
},
);
@@ -100,7 +97,7 @@ describe('RelatedIssuesBlock', () => {
slots: { 'header-actions': headerActions },
});
- expect(wrapper.find('[data-testid="custom-button"]').html()).toBe(headerActions);
+ expect(wrapper.findByTestId('custom-button').html()).toBe(headerActions);
});
});
@@ -260,15 +257,30 @@ describe('RelatedIssuesBlock', () => {
});
});
- it('toggle button is disabled when issue has no related items', () => {
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [],
- issuableType: 'issue',
- },
- });
+ describe('empty state', () => {
+ it.each`
+ issuableType | pathIdSeparator | showCategorizedIssues | emptyText | helpLinkText
+ ${'issue'} | ${PathIdSeparator.Issue} | ${false} | ${"Link issues together to show that they're related."} | ${'Learn more about linking issues'}
+ ${'issue'} | ${PathIdSeparator.Issue} | ${true} | ${"Link issues together to show that they're related or that one is blocking others."} | ${'Learn more about linking issues'}
+ ${'incident'} | ${PathIdSeparator.Issue} | ${false} | ${"Link incidents together to show that they're related."} | ${'Learn more about linking issues and incidents'}
+ ${'incident'} | ${PathIdSeparator.Issue} | ${true} | ${"Link incidents together to show that they're related or that one is blocking others."} | ${'Learn more about linking issues and incidents'}
+ ${'epic'} | ${PathIdSeparator.Epic} | ${true} | ${"Link epics together to show that they're related or that one is blocking others."} | ${'Learn more about linking epics'}
+ `(
+ 'displays "$emptyText" in the body and "$helpLinkText" aria-label for help link',
+ ({ issuableType, pathIdSeparator, showCategorizedIssues, emptyText, helpLinkText }) => {
+ wrapper = mountExtended(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator,
+ issuableType,
+ canAdmin: true,
+ helpPath: '/help/user/project/issues/related_issues',
+ showCategorizedIssues,
+ },
+ });
- expect(findToggleButton().props('disabled')).toBe(true);
+ expect(wrapper.findByTestId('related-issues-body').text()).toContain(emptyText);
+ expect(wrapper.findByTestId('help-link').attributes('aria-label')).toBe(helpLinkText);
+ },
+ );
});
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index 680dbd68493..bedf8bcaf34 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -201,18 +201,20 @@ describe('RelatedIssuesRoot', () => {
]);
});
- it('displays a message from the backend upon error', async () => {
+ it('passes an error message from the backend upon error', async () => {
const input = '#123';
const message = 'error';
mock.onPost(defaultProps.endpoint).reply(409, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
- expect(createAlert).not.toHaveBeenCalled();
+ expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
+ expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(null);
findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
await waitForPromises();
- expect(createAlert).toHaveBeenCalledWith({ message });
+ expect(findRelatedIssuesBlock().props('hasError')).toBe(true);
+ expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(message);
});
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
new file mode 100644
index 00000000000..3f72396cce6
--- /dev/null
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -0,0 +1,58 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
+
+describe('IssuesDashboardApp component', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ calendarPath: 'calendar/path',
+ emptyStateSvgPath: 'empty-state.svg',
+ isSignedIn: true,
+ rssPath: 'rss/path',
+ };
+
+ const findCalendarButton = () =>
+ wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.calendarButtonText });
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findRssButton = () =>
+ wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText });
+
+ const mountComponent = () => {
+ wrapper = mountExtended(IssuesDashboardApp, { provide: defaultProvide });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders IssuableList component', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ currentTab: IssuableStates.Opened,
+ namespace: 'dashboard',
+ recentSearchesStorageKey: 'issues',
+ searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ tabs: IssuesDashboardApp.IssuableListTabs,
+ });
+ });
+
+ it('renders RSS button link', () => {
+ expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
+ expect(findRssButton().props('icon')).toBe('rss');
+ });
+
+ it('renders calendar button link', () => {
+ expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ expect(findCalendarButton().props('icon')).toBe('calendar');
+ });
+
+ it('renders empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultProvide.emptyStateSvgPath,
+ title: IssuesDashboardApp.i18n.emptyStateTitle,
+ });
+ });
+});
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 5133c02b190..d0c93c896b3 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -33,16 +33,6 @@ import {
CREATED_DESC,
RELATIVE_POSITION,
RELATIVE_POSITION_ASC,
- TOKEN_TYPE_ASSIGNEE,
- 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_TYPE,
urlSortParams,
} from '~/issues/list/constants';
import eventHub from '~/issues/list/eventhub';
@@ -57,7 +47,19 @@ import {
WORK_ITEM_TYPE_ENUM_TASK,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
} from '~/work_items/constants';
-import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ TOKEN_TYPE_ASSIGNEE,
+ 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_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import('~/issuable/bulk_update_sidebar');
import('~/users_select');
@@ -89,7 +91,6 @@ describe('CE IssuesListApp component', () => {
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
- hasMultipleIssueAssigneesFeature: true,
hasScopedLabelsFeature: true,
initialEmail: 'email@example.com',
initialSort: CREATED_DESC,
@@ -131,7 +132,6 @@ describe('CE IssuesListApp component', () => {
const mountComponent = ({
provide = {},
data = {},
- workItems = false,
issuesQueryResponse = mockIssuesQueryResponse,
issuesCountsQueryResponse = mockIssuesCountsQueryResponse,
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
@@ -150,9 +150,6 @@ describe('CE IssuesListApp component', () => {
apolloProvider: createMockApollo(requestHandlers),
router,
provide: {
- glFeatures: {
- workItems,
- },
...defaultProvide,
...provide,
},
@@ -605,17 +602,20 @@ describe('CE IssuesListApp component', () => {
beforeEach(() => {
wrapper = mountComponent({
provide: { hasAnyIssues: false, isSignedIn: false },
+ mountFn: mount,
});
});
it('shows empty state', () => {
expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noIssuesSignedOutDescription,
title: IssuesListApp.i18n.noIssuesSignedOutTitle,
svgPath: defaultProvide.emptyStateSvgPath,
primaryButtonText: IssuesListApp.i18n.noIssuesSignedOutButtonText,
primaryButtonLink: defaultProvide.signInPath,
});
+ expect(findGlEmptyState().text()).toContain(
+ IssuesListApp.i18n.noIssuesSignedOutDescription,
+ );
});
});
});
@@ -1060,45 +1060,23 @@ describe('CE IssuesListApp component', () => {
});
describe('fetching issues', () => {
- describe('when work_items feature flag is disabled', () => {
- beforeEach(() => {
- wrapper = mountComponent({ workItems: false });
- jest.runOnlyPendingTimers();
- });
-
- it('fetches issue, incident, and test case types', () => {
- const types = [
- WORK_ITEM_TYPE_ENUM_ISSUE,
- WORK_ITEM_TYPE_ENUM_INCIDENT,
- WORK_ITEM_TYPE_ENUM_TEST_CASE,
- ];
-
- expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
- expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({ types }),
- );
- });
+ beforeEach(() => {
+ wrapper = mountComponent();
+ jest.runOnlyPendingTimers();
});
- describe('when work_items feature flag is enabled', () => {
- beforeEach(() => {
- wrapper = mountComponent({ workItems: true });
- jest.runOnlyPendingTimers();
- });
-
- it('fetches issue, incident, test case, and task types', () => {
- const types = [
- WORK_ITEM_TYPE_ENUM_ISSUE,
- WORK_ITEM_TYPE_ENUM_INCIDENT,
- WORK_ITEM_TYPE_ENUM_TEST_CASE,
- WORK_ITEM_TYPE_ENUM_TASK,
- ];
-
- expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
- expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({ types }),
- );
- });
+ it('fetches issue, incident, test case, and task types', () => {
+ const types = [
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ ];
+
+ expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
+ expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
+ expect.objectContaining({ types }),
+ );
});
});
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 42e9d348b16..62fcbf7aad0 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -1,6 +1,21 @@
import {
+ FILTERED_SEARCH_TERM,
OPERATOR_IS,
OPERATOR_IS_NOT,
+ OPERATOR_OR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_EPIC,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getIssuesQueryResponse = {
@@ -122,6 +137,8 @@ export const locationSearch = [
'assignee_username[]=5',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
+ 'or[assignee_username][]=carl',
+ 'or[assignee_username][]=lenny',
'milestone_title=season+3',
'milestone_title=season+4',
'not[milestone_title]=season+20',
@@ -166,56 +183,58 @@ export const locationSearchWithSpecialValues = [
].join('&');
export const filteredTokens = [
- { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } },
- { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
- { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: '5', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
- { type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
- { type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } },
- { type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } },
- { type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
- { type: 'milestone', value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
- { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } },
- { type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } },
- { type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } },
- { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
- { type: 'release', value: { data: 'v3', operator: OPERATOR_IS } },
- { type: 'release', value: { data: 'v4', operator: OPERATOR_IS } },
- { type: 'release', value: { data: 'v20', operator: OPERATOR_IS_NOT } },
- { type: 'release', value: { data: 'v30', operator: OPERATOR_IS_NOT } },
- { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'feature', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'bug', operator: OPERATOR_IS_NOT } },
- { type: 'type', value: { data: 'incident', operator: OPERATOR_IS_NOT } },
- { type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
- { type: 'my_reaction_emoji', value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
- { type: 'confidential', value: { data: 'yes', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: '12', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
- { type: 'iteration', value: { data: '42', operator: OPERATOR_IS_NOT } },
- { type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
- { type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
- { type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
- { type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
- { type: 'crm_contact', value: { data: '123', operator: OPERATOR_IS } },
- { type: 'crm_organization', value: { data: '456', operator: OPERATOR_IS } },
- { type: 'filtered-search-term', value: { data: 'find' } },
- { type: 'filtered-search-term', value: { data: 'issues' } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'homer', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_IS_NOT } },
+ { 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: '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_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_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_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_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_IS_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_EPIC, value: { data: '12', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_IS_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_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'issues' } },
];
export const filteredTokensWithSpecialValues = [
- { type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
- { type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } },
- { type: 'labels', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'release', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'milestone', value: { data: 'Upcoming', operator: OPERATOR_IS } },
- { type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'weight', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: '123', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: 'Current', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'Upcoming', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_EPIC, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: 'None', operator: OPERATOR_IS } },
];
export const apiParams = {
@@ -244,6 +263,9 @@ export const apiParams = {
epicId: '34',
weight: '3',
},
+ or: {
+ assigneeUsernames: ['carl', 'lenny'],
+ },
};
export const apiParamsWithSpecialValues = {
@@ -263,6 +285,7 @@ export const urlParams = {
'not[author_username]': 'marge',
'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
+ 'or[assignee_username][]': ['carl', 'lenny'],
milestone_title: ['season 3', 'season 4'],
'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index cd4d422583b..273ddfdd5d4 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -86,7 +86,7 @@ describe('Description field component', () => {
renderMarkdownPath: '/',
markdownDocsPath: '/',
quickActionsDocsPath: expect.any(String),
- initOnAutofocus: true,
+ autofocus: true,
supportsQuickActions: true,
enableAutocomplete: true,
}),
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 2e7449974e5..0ce3f75f576 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
@@ -52,6 +52,9 @@ describe('Timeline events form', () => {
findMinuteInput().setValue(45);
};
const findTextarea = () => wrapper.findByTestId('input-note');
+ const findCountNumeric = (count) => wrapper.findByText(count);
+ const findCountVerbose = (count) => wrapper.findByText(`${count} characters remaining`);
+ const findCountHint = () => wrapper.findByText(timelineFormI18n.hint);
const submitForm = async () => {
findSubmitButton().vm.$emit('click');
@@ -135,4 +138,31 @@ describe('Timeline events form', () => {
expect(findSubmitAndAddButton().props('disabled')).toBe(false);
});
});
+
+ describe('form character limit', () => {
+ beforeEach(() => {
+ mountComponent({ mountMethod: mountExtended });
+ });
+
+ it('sets a character limit hint', () => {
+ expect(findCountHint().exists()).toBe(true);
+ });
+
+ it('sets a character limit when text is entered', async () => {
+ await findTextarea().setValue('hello');
+
+ expect(findCountNumeric('275').text()).toBe('275');
+ expect(findCountVerbose('275').text()).toBe('275 characters remaining');
+ });
+
+ it('prevents form submission when text is beyond maximum length', async () => {
+ // 281 characters long
+ const longText =
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in volupte';
+ await findTextarea().setValue(longText);
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ expect(findSubmitAndAddButton().props('disabled')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
index cb32ca9d3dc..95eb10118ee 100644
--- a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
@@ -3,7 +3,7 @@ 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 from '../../mock_data';
+import job, { failedJobStatus } from '../../mock_data';
describe('Legacy Sidebar Header', () => {
let store;
@@ -67,6 +67,12 @@ describe('Legacy Sidebar Header', () => {
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', () => {
@@ -88,4 +94,16 @@ describe('Legacy Sidebar Header', () => {
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/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js
index dc1aa67489d..27911eb76eb 100644
--- a/spec/frontend/jobs/components/job/sidebar_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_spec.js
@@ -1,6 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue';
import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue';
import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue';
@@ -10,6 +13,7 @@ import createStore from '~/jobs/store';
import job, { jobsInStage } from '../../mock_data';
describe('Sidebar details block', () => {
+ let mock;
let store;
let wrapper;
@@ -18,6 +22,8 @@ describe('Sidebar details block', () => {
const findArtifactsBlock = () => wrapper.findComponent(ArtifactsBlock);
const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
const findTerminalLink = () => wrapper.findByTestId('terminal-link');
+ const findJobStagesDropdown = () => wrapper.findComponent(StagesDropdown);
+ const findJobsContainer = () => wrapper.findComponent(JobsContainer);
const createWrapper = (props) => {
store = createStore();
@@ -35,6 +41,13 @@ describe('Sidebar details block', () => {
);
};
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet().reply(httpStatus.OK, {
+ name: job.stage,
+ });
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -110,31 +123,72 @@ describe('Sidebar details block', () => {
describe('stages dropdown', () => {
beforeEach(() => {
createWrapper();
- return store.dispatch('receiveJobSuccess', { ...job, stage: 'aStage' });
+ return store.dispatch('receiveJobSuccess', job);
});
describe('with stages', () => {
it('renders value provided as selectedStage as selected', () => {
- expect(wrapper.findComponent(StagesDropdown).props('selectedStage')).toBe('aStage');
+ expect(findJobStagesDropdown().props('selectedStage')).toBe(job.stage);
});
});
describe('without jobs for stages', () => {
- beforeEach(() => store.dispatch('receiveJobSuccess', job));
-
it('does not render jobs container', () => {
- expect(wrapper.findComponent(JobsContainer).exists()).toBe(false);
+ expect(findJobsContainer().exists()).toBe(false);
});
});
describe('with jobs for stages', () => {
+ beforeEach(() => {
+ return store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
+ });
+
+ it('renders list of jobs', async () => {
+ expect(findJobsContainer().exists()).toBe(true);
+ });
+ });
+
+ describe('when job data changes', () => {
+ const stageArg = job.pipeline.details.stages.find((stage) => stage.name === job.stage);
+
beforeEach(async () => {
- await store.dispatch('receiveJobSuccess', job);
- await store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
+ jest.spyOn(store, 'dispatch');
});
- it('renders list of jobs', () => {
- expect(wrapper.findComponent(JobsContainer).exists()).toBe(true);
+ describe('and the job stage is currently selected', () => {
+ describe('when the status changed', () => {
+ it('refetch the jobs list for the stage', async () => {
+ await store.dispatch('receiveJobSuccess', { ...job, status: 'new' });
+
+ expect(store.dispatch).toHaveBeenNthCalledWith(2, 'fetchJobsForStage', { ...stageArg });
+ });
+ });
+
+ describe('when the status did not change', () => {
+ it('does not refetch the jobs list for the stage', async () => {
+ await store.dispatch('receiveJobSuccess', { ...job });
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenNthCalledWith(1, 'receiveJobSuccess', {
+ ...job,
+ });
+ });
+ });
+ });
+
+ describe('and the job stage is not currently selected', () => {
+ it('does not refetch the jobs list for the stage', async () => {
+ // Setting stage to `random` on the job means that we are looking
+ // at `build` stage currently, but the job we are seeing in the logs
+ // belong to `random`, so we shouldn't have to refetch
+ await store.dispatch('receiveJobSuccess', { ...job, stage: 'random' });
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenNthCalledWith(1, 'receiveJobSuccess', {
+ ...job,
+ stage: 'random',
+ });
+ });
});
});
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index bf238b2e39a..a7fe6d5a626 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -925,6 +925,7 @@ export default {
locked: false,
},
name: 'test',
+ stage: 'build',
build_path: '/root/ci-mock/-/jobs/4757',
retry_path: '/root/ci-mock/-/jobs/4757/retry',
cancel_path: '/root/ci-mock/-/jobs/4757/cancel',
@@ -1085,6 +1086,29 @@ export default {
has_trace: true,
};
+export const failedJobStatus = {
+ icon: 'status_warning',
+ text: 'failed',
+ label: 'failed (allowed to fail)',
+ group: 'failed-with-warnings',
+ tooltip: 'failed - (unknown failure) (allowed to fail)',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-shell/-/jobs/454',
+ illustration: {
+ image: 'illustrations/skipped-job_empty.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/gitlab-org/gitlab-shell/-/jobs/454/retry',
+ method: 'post',
+ },
+};
+
export const jobsInStage = {
name: 'build',
title: 'build: running',
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index a0140d1d8a8..947c38c8ae8 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1016,45 +1016,6 @@ describe('common_utils', () => {
});
});
- describe('searchBy', () => {
- const searchSpace = {
- iid: 1,
- reference: '&1',
- title: 'Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate.',
- url: '/groups/gitlab-org/-/epics/1',
- };
-
- it('returns null when `query` or `searchSpace` params are empty/undefined', () => {
- expect(commonUtils.searchBy('omnis', null)).toBeNull();
- expect(commonUtils.searchBy('', searchSpace)).toBeNull();
- expect(commonUtils.searchBy()).toBeNull();
- });
-
- it('returns object with matching props based on `query` & `searchSpace` params', () => {
- // String `omnis` is found only in `title` prop so return just that
- expect(commonUtils.searchBy('omnis', searchSpace)).toEqual(
- expect.objectContaining({
- title: searchSpace.title,
- }),
- );
-
- // String `1` is found in both `iid` and `reference` props so return both
- expect(commonUtils.searchBy('1', searchSpace)).toEqual(
- expect.objectContaining({
- iid: searchSpace.iid,
- reference: searchSpace.reference,
- }),
- );
-
- // String `/epics/1` is found in `url` prop so return just that
- expect(commonUtils.searchBy('/epics/1', searchSpace)).toEqual(
- expect.objectContaining({
- url: searchSpace.url,
- }),
- );
- });
- });
-
describe('isScopedLabel', () => {
it('returns true when `::` is present in title', () => {
expect(commonUtils.isScopedLabel({ title: 'foo::bar' })).toBe(true);
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
new file mode 100644
index 00000000000..142c76f7bc0
--- /dev/null
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
@@ -0,0 +1,103 @@
+import Vue, { nextTick } from 'vue';
+import { createWrapper } from '@vue/test-utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
+
+const originalMount = Vue.prototype.$mount;
+
+describe('confirmAction', () => {
+ let modalWrapper;
+ let confirActionPromise;
+ let modal;
+
+ const findConfirmModal = () => modalWrapper.findComponent(ConfirmModal);
+ const renderRootComponent = async (message, opts) => {
+ confirActionPromise = confirmAction(message, opts);
+ // We have to wait for two ticks here.
+ // The first one is to wait for rendering of the root component
+ // The second one to wait for rendering of the dynamically
+ // loaded confirm-modal component
+ await nextTick();
+ await nextTick();
+ modal = findConfirmModal();
+ };
+ const mockMount = (vm, el) => {
+ originalMount.call(vm, el);
+ modalWrapper = createWrapper(vm);
+ return vm;
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="component"></div>');
+ const el = document.getElementById('component');
+ // We mock the implementation only once to make sure that we mock
+ // it only for the root component in confirm_action.
+ // Mounting other components (like confirm-modal) should not be affected with
+ // this mock
+ jest.spyOn(Vue.prototype, '$mount').mockImplementationOnce(function mock() {
+ return mockMount(this, el);
+ });
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ Vue.prototype.$mount.mockRestore();
+ modalWrapper?.destroy();
+ modalWrapper = null;
+ modal?.destroy();
+ modal = null;
+ });
+
+ it('creats a ConfirmModal with message as slot', async () => {
+ const message = 'Bonjour le monde!';
+ await renderRootComponent(message);
+
+ expect(modal.vm.$slots.default[0].text).toBe(message);
+ });
+
+ it('creats a ConfirmModal with props', async () => {
+ const options = {
+ primaryBtnText: 'primaryBtnText',
+ primaryBtnVariant: 'info',
+ secondaryBtnText: 'secondaryBtnText',
+ secondaryBtnVariant: 'success',
+ cancelBtnText: 'cancelBtnText',
+ cancelBtnVariant: 'danger',
+ modalHtmlMessage: '<strong>Hello</strong>',
+ title: 'title',
+ hideCancel: true,
+ };
+ await renderRootComponent('', options);
+ expect(modal.props()).toEqual(
+ expect.objectContaining({
+ primaryText: options.primaryBtnText,
+ primaryVariant: options.primaryBtnVariant,
+ secondaryText: options.secondaryBtnText,
+ secondaryVariant: options.secondaryBtnVariant,
+ cancelText: options.cancelBtnText,
+ cancelVariant: options.cancelBtnVariant,
+ modalHtmlMessage: options.modalHtmlMessage,
+ title: options.title,
+ hideCancel: options.hideCancel,
+ }),
+ );
+ });
+
+ it('resolves promise when modal emit `closed`', async () => {
+ await renderRootComponent('');
+
+ modal.vm.$emit('closed');
+
+ await expect(confirActionPromise).resolves.toBe(false);
+ });
+
+ it('confirms when modal emit `confirmed` before `closed`', async () => {
+ await renderRootComponent('');
+
+ modal.vm.$emit('confirmed');
+ modal.vm.$emit('closed');
+
+ await expect(confirActionPromise).resolves.toBe(true);
+ });
+});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js
new file mode 100644
index 00000000000..6966c79b232
--- /dev/null
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js
@@ -0,0 +1,80 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { confirmViaGlModal } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_action');
+
+describe('confirmViaGlModal', () => {
+ let el;
+
+ afterEach(() => {
+ el = undefined;
+ resetHTMLFixture();
+ jest.resetAllMocks();
+ });
+
+ const createElement = (html) => {
+ setHTMLFixture(html);
+ return document.body.firstChild;
+ };
+
+ it('returns confirmAction result', async () => {
+ confirmAction.mockReturnValue(Promise.resolve(true));
+ el = createElement(`<div/>`);
+
+ await expect(confirmViaGlModal('', el)).resolves.toBe(true);
+ });
+
+ it('calls confirmAction with message', () => {
+ el = createElement(`<div/>`);
+
+ confirmViaGlModal('message', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('message', {});
+ });
+
+ it.each(['gl-sr-only', 'sr-only'])(
+ `uses slot.%s contentText as primaryBtnText`,
+ (srOnlyClass) => {
+ el = createElement(
+ `<a href="#"><span class="${srOnlyClass}">Delete merge request</span></a>`,
+ );
+
+ confirmViaGlModal('', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('', {
+ primaryBtnText: 'Delete merge request',
+ });
+ },
+ );
+
+ it('uses `aria-label` value as `primaryBtnText`', () => {
+ el = createElement(`<a aria-label="Delete merge request" href="#"></a>`);
+
+ confirmViaGlModal('', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('', {
+ primaryBtnText: 'Delete merge request',
+ });
+ });
+
+ it.each([
+ ['title', 'title', 'Delete?'],
+ ['confirm-btn-variant', `primaryBtnVariant`, 'danger'],
+ ])('uses data-%s value as confirmAction config', (dataKey, configKey, value) => {
+ el = createElement(`<a data-${dataKey}="${value}" href="#"></a>`);
+
+ confirmViaGlModal('message', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('message', { [configKey]: value });
+ });
+
+ it('uses message as modalHtmlMessage value when data-is-html-message is true', () => {
+ el = createElement(`<a data-is-html-message="true" href="#"></a>`);
+ const message = 'Hola mundo!';
+
+ confirmViaGlModal(message, el);
+
+ expect(confirmAction).toHaveBeenCalledWith(message, { modalHtmlMessage: message });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
index 59b3b4c02df..055d57d6ada 100644
--- a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
@@ -1,4 +1,9 @@
-import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
+import {
+ getDateWithUTC,
+ newDateAsLocaleTime,
+ nSecondsAfter,
+ nSecondsBefore,
+} from '~/lib/utils/datetime/date_calculation_utility';
describe('newDateAsLocaleTime', () => {
it.each`
@@ -31,3 +36,33 @@ describe('getDateWithUTC', () => {
expect(getDateWithUTC(date)).toEqual(expected);
});
});
+
+describe('nSecondsAfter', () => {
+ const start = new Date('2022-03-22T01:23:45.678Z');
+ it.each`
+ date | seconds | expected
+ ${start} | ${0} | ${start}
+ ${start} | ${1} | ${new Date('2022-03-22T01:23:46.678Z')}
+ ${start} | ${5} | ${new Date('2022-03-22T01:23:50.678Z')}
+ ${start} | ${60} | ${new Date('2022-03-22T01:24:45.678Z')}
+ ${start} | ${3600} | ${new Date('2022-03-22T02:23:45.678Z')}
+ ${start} | ${86400} | ${new Date('2022-03-23T01:23:45.678Z')}
+ `('returns $expected given $string', ({ date, seconds, expected }) => {
+ expect(nSecondsAfter(date, seconds)).toEqual(expected);
+ });
+});
+
+describe('nSecondsBefore', () => {
+ const start = new Date('2022-03-22T01:23:45.678Z');
+ it.each`
+ date | seconds | expected
+ ${start} | ${0} | ${start}
+ ${start} | ${1} | ${new Date('2022-03-22T01:23:44.678Z')}
+ ${start} | ${5} | ${new Date('2022-03-22T01:23:40.678Z')}
+ ${start} | ${60} | ${new Date('2022-03-22T01:22:45.678Z')}
+ ${start} | ${3600} | ${new Date('2022-03-22T00:23:45.678Z')}
+ ${start} | ${86400} | ${new Date('2022-03-21T01:23:45.678Z')}
+ `('returns $expected given $string', ({ date, seconds, expected }) => {
+ expect(nSecondsBefore(date, seconds)).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index b537e6b2bf8..d6bac935970 100644
--- a/spec/frontend/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -1,8 +1,10 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
import {
addClassIfElementExists,
canScrollUp,
canScrollDown,
+ getContentWrapperHeight,
parseBooleanDataAttributes,
isElementVisible,
getParents,
@@ -235,4 +237,30 @@ describe('DOM Utils', () => {
expect(div.getAttribute('title')).toBe('another test');
});
});
+
+ describe('getContentWrapperHeight', () => {
+ const fixture = `
+ <div>
+ <div class="content-wrapper">
+ <div class="content"></div>
+ </div>
+ </div>
+ `;
+
+ beforeEach(() => {
+ setHTMLFixture(fixture);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('returns the height of an element that exists', () => {
+ expect(getContentWrapperHeight('.content-wrapper')).toBe('0px');
+ });
+
+ it('returns an empty string for a class that does not exist', () => {
+ expect(getContentWrapperHeight('.does-not-exist')).toBe('');
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/unit_format/index_spec.js b/spec/frontend/lib/utils/unit_format/index_spec.js
index dc9d6ece48e..057d7aded02 100644
--- a/spec/frontend/lib/utils/unit_format/index_spec.js
+++ b/spec/frontend/lib/utils/unit_format/index_spec.js
@@ -2,6 +2,7 @@ import {
number,
percent,
percentHundred,
+ days,
seconds,
milliseconds,
decimalBytes,
@@ -72,6 +73,11 @@ describe('unit_format', () => {
expect(percentHundred(1000)).toBe('1,000%');
});
+ it('days', () => {
+ expect(days(1)).toBe('1d');
+ expect(days(1, undefined, { unitSeparator: '/' })).toBe('1/d');
+ });
+
it('seconds', () => {
expect(seconds(1)).toBe('1s');
expect(seconds(1, undefined, { unitSeparator: ' ' })).toBe('1 s');
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index 3dac47974e7..df5c884f42e 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -24,86 +24,52 @@ describe('AccessRequestActionButtons', () => {
wrapper.destroy();
});
- describe('when user has `canRemove` permissions', () => {
- beforeEach(() => {
- createComponent({
- permissions: {
- canRemove: true,
- },
- });
- });
+ it('renders remove member button', () => {
+ createComponent();
- it('renders remove member button', () => {
- expect(findRemoveMemberButton().exists()).toBe(true);
- });
-
- it('sets props correctly', () => {
- expect(findRemoveMemberButton().props()).toMatchObject({
- memberId: member.id,
- title: 'Deny access',
- isAccessRequest: true,
- isInvite: false,
- icon: 'close',
- });
- });
-
- describe('when member is the current user', () => {
- it('sets `message` prop correctly', () => {
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
- );
- });
- });
+ expect(findRemoveMemberButton().exists()).toBe(true);
+ });
- describe('when member is not the current user', () => {
- it('sets `message` prop correctly', () => {
- createComponent({
- isCurrentUser: false,
- permissions: {
- canRemove: true,
- },
- });
+ it('sets props correctly on remove member button', () => {
+ createComponent();
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
- );
- });
+ expect(findRemoveMemberButton().props()).toMatchObject({
+ memberId: member.id,
+ title: 'Deny access',
+ isAccessRequest: true,
+ isInvite: false,
+ icon: 'close',
});
});
- describe('when user does not have `canRemove` permissions', () => {
- it('does not render remove member button', () => {
- createComponent({
- permissions: {
- canRemove: false,
- },
- });
+ describe('when member is the current user', () => {
+ it('sets `message` prop correctly', () => {
+ createComponent();
- expect(findRemoveMemberButton().exists()).toBe(false);
+ expect(findRemoveMemberButton().props('message')).toBe(
+ `Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
+ );
});
});
- describe('when user has `canUpdate` permissions', () => {
- it('renders the approve button', () => {
+ describe('when member is not the current user', () => {
+ it('sets `message` prop correctly', () => {
createComponent({
+ isCurrentUser: false,
permissions: {
- canUpdate: true,
+ canRemove: true,
},
});
- expect(findApproveButton().exists()).toBe(true);
+ expect(findRemoveMemberButton().props('message')).toBe(
+ `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
+ );
});
});
- describe('when user does not have `canUpdate` permissions', () => {
- it('does not render the approve button', () => {
- createComponent({
- permissions: {
- canUpdate: false,
- },
- });
+ it('renders the approve button', () => {
+ createComponent();
- expect(findApproveButton().exists()).toBe(false);
- });
+ expect(findApproveButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 1354b938d77..77af5e7293e 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -81,6 +81,7 @@ describe('MembersTabs', () => {
stubs: ['members-app'],
provide: {
canManageMembers: true,
+ canManageAccessRequests: true,
canExportMembers: true,
exportCsvPath: '',
...provide,
@@ -181,7 +182,9 @@ describe('MembersTabs', () => {
describe('when `canManageMembers` is `false`', () => {
it('shows all tabs except `Invited` and `Access requests`', async () => {
- await createComponent({ provide: { canManageMembers: false } });
+ await createComponent({
+ provide: { canManageMembers: false, canManageAccessRequests: false },
+ });
expect(findTabByText('Members')).not.toBeUndefined();
expect(findTabByText('Groups')).not.toBeUndefined();
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap
new file mode 100644
index 00000000000..2eba8869535
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap
@@ -0,0 +1,223 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShowExperiment with candidates 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/groups/gitlab-org/-/epics/8560"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Feedback and Updates
+
+ </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>
+
+ Experiment Candidates
+
+ </h3>
+
+ <table
+ aria-busy="false"
+ aria-colcount="4"
+ class="table b-table gl-table gl-mt-0!"
+ role="table"
+ >
+ <!---->
+ <!---->
+ <thead
+ class=""
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <th
+ aria-colindex="1"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ L1 Ratio
+ </div>
+ </th>
+ <th
+ aria-colindex="2"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Rmse
+ </div>
+ </th>
+ <th
+ aria-colindex="3"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Auc
+ </div>
+ </th>
+ <th
+ aria-colindex="4"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Mae
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ 0.4
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+ 1
+ </td>
+ <td
+ aria-colindex="3"
+ class=""
+ role="cell"
+ />
+ <td
+ aria-colindex="4"
+ class=""
+ role="cell"
+ />
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ 0.5
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ />
+ <td
+ aria-colindex="3"
+ class=""
+ role="cell"
+ >
+ 0.3
+ </td>
+ <td
+ aria-colindex="4"
+ class=""
+ role="cell"
+ />
+ </tr>
+ <!---->
+ <!---->
+ </tbody>
+ <!---->
+ </table>
+</div>
+`;
diff --git a/spec/frontend/ml/experiment_tracking/components/experiment_spec.js b/spec/frontend/ml/experiment_tracking/components/experiment_spec.js
new file mode 100644
index 00000000000..af722d77532
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/experiment_spec.js
@@ -0,0 +1,44 @@
+import { GlAlert } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
+
+describe('ShowExperiment', () => {
+ let wrapper;
+
+ const createWrapper = (candidates = [], metricNames = [], paramNames = []) => {
+ return mountExtended(ShowExperiment, { provide: { candidates, metricNames, paramNames } });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const findEmptyState = () => wrapper.findByText('This Experiment has no logged Candidates');
+
+ it('shows incubation warning', () => {
+ wrapper = createWrapper();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('no candidates', () => {
+ it('shows empty state', () => {
+ wrapper = createWrapper();
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+
+ describe('with candidates', () => {
+ it('renders correctly', () => {
+ wrapper = createWrapper(
+ [
+ { rmse: 1, l1_ratio: 0.4 },
+ { auc: 0.3, l1_ratio: 0.5 },
+ ],
+ ['rmse', 'auc', 'mae'],
+ ['l1_ratio'],
+ );
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
new file mode 100644
index 00000000000..e07a4ed816b
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
@@ -0,0 +1,27 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import IncubationAlert from '~/ml/experiment_tracking/components/incubation_alert.vue';
+
+describe('IncubationAlert', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ wrapper = mount(IncubationAlert);
+ });
+
+ it('displays link to issue', () => {
+ expect(findButton().attributes().href).toBe(
+ 'https://gitlab.com/groups/gitlab-org/-/epics/8560',
+ );
+ });
+
+ it('is removed if dismissed', async () => {
+ await wrapper.find('[aria-label="Dismiss"]').trigger('click');
+
+ expect(findAlert().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index c757b55faf4..a7776bd5b69 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -5,20 +5,22 @@ import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json';
import basicJson from 'test_fixtures/blob/notebook/basic.json';
import mathJson from 'test_fixtures/blob/notebook/math.json';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
const Component = Vue.extend(MarkdownComponent);
window.katex = katex;
-function buildCellComponent(cell, relativePath = '') {
+function buildCellComponent(cell, relativePath = '', hidePrompt) {
return mount(Component, {
propsData: {
cell,
+ hidePrompt,
},
provide: {
relativeRawPath: relativePath,
},
- }).vm;
+ });
}
function buildMarkdownComponent(markdownContent, relativePath = '') {
@@ -33,7 +35,7 @@ function buildMarkdownComponent(markdownContent, relativePath = '') {
}
describe('Markdown component', () => {
- let vm;
+ let wrapper;
let cell;
let json;
@@ -43,21 +45,30 @@ describe('Markdown component', () => {
// eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
- vm = buildCellComponent(cell);
+ wrapper = buildCellComponent(cell);
await nextTick();
});
- it('does not render prompt', () => {
- expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ const findPrompt = () => wrapper.findComponent(Prompt);
+
+ it('renders a prompt by default', () => {
+ expect(findPrompt().exists()).toBe(true);
+ });
+
+ it('does not render a prompt if hidePrompt is true', () => {
+ wrapper = buildCellComponent(cell, '', true);
+ expect(findPrompt().exists()).toBe(false);
});
it('does not render the markdown text', () => {
- expect(vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(cell.source.join(''));
+ expect(wrapper.vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(
+ cell.source.join(''),
+ );
});
it('renders the markdown HTML', () => {
- expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('.markdown h1')).not.toBeNull();
});
it('sanitizes Markdown output', async () => {
@@ -68,11 +79,11 @@ describe('Markdown component', () => {
});
await nextTick();
- expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
+ expect(wrapper.vm.$el.querySelector('a').getAttribute('href')).toBeNull();
});
it('sanitizes HTML', async () => {
- const findLink = () => vm.$el.querySelector('.xss-link');
+ const findLink = () => wrapper.vm.$el.querySelector('.xss-link');
Object.assign(cell, {
source: ['<a href="test.js" data-remote=true data-type="script" class="xss-link">XSS</a>\n'],
});
@@ -97,11 +108,11 @@ describe('Markdown component', () => {
["for embedded images, it doesn't", '![](data:image/jpeg;base64)\n', 'src="data:'],
["for images urls, it doesn't", '![](http://image.png)\n', 'src="http:'],
])('%s', async ([testMd, mustContain]) => {
- vm = buildMarkdownComponent([testMd], '/raw/');
+ wrapper = buildMarkdownComponent([testMd], '/raw/');
await nextTick();
- expect(vm.$el.innerHTML).toContain(mustContain);
+ expect(wrapper.vm.$el.innerHTML).toContain(mustContain);
});
});
@@ -111,13 +122,13 @@ describe('Markdown component', () => {
});
it('renders images and text', async () => {
- vm = buildCellComponent(json.cells[0]);
+ wrapper = buildCellComponent(json.cells[0]);
await nextTick();
- const images = vm.$el.querySelectorAll('img');
+ const images = wrapper.vm.$el.querySelectorAll('img');
expect(images.length).toBe(5);
- const columns = vm.$el.querySelectorAll('td');
+ const columns = wrapper.vm.$el.querySelectorAll('td');
expect(columns.length).toBe(6);
expect(columns[0].textContent).toEqual('Hello ');
@@ -141,81 +152,93 @@ describe('Markdown component', () => {
});
it('renders multi-line katex', async () => {
- vm = buildCellComponent(json.cells[0]);
+ wrapper = buildCellComponent(json.cells[0]);
await nextTick();
- expect(vm.$el.querySelector('.katex')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('.katex')).not.toBeNull();
});
it('renders inline katex', async () => {
- vm = buildCellComponent(json.cells[1]);
+ wrapper = buildCellComponent(json.cells[1]);
await nextTick();
- expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
});
it('renders multiple inline katex', async () => {
- vm = buildCellComponent(json.cells[1]);
+ wrapper = buildCellComponent(json.cells[1]);
await nextTick();
- expect(vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
+ expect(wrapper.vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
});
it('output cell in case of katex error', async () => {
- vm = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
+ wrapper = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
await nextTick();
// expect one paragraph with no katex formula in it
- expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(0);
+ expect(wrapper.vm.$el.querySelectorAll('p')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p .katex')).toHaveLength(0);
});
it('output cell and render remaining formula in case of katex error', async () => {
- vm = buildMarkdownComponent([
+ wrapper = buildMarkdownComponent([
'An invalid $a & b$ inline formula and a vaild one $b = c$\n',
'\n',
]);
await nextTick();
// expect one paragraph with no katex formula in it
- expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p .katex')).toHaveLength(1);
});
it('renders math formula in list object', async () => {
- vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
+ wrapper = buildMarkdownComponent([
+ "- list with inline $a=2$ inline formula $a' + b = c$\n",
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it("renders math formula with tick ' in it", async () => {
- vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
+ wrapper = buildMarkdownComponent([
+ "- list with inline $a=2$ inline formula $a' + b = c$\n",
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it('renders math formula with less-than-operator < in it', async () => {
- vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b < c$\n', '\n']);
+ wrapper = buildMarkdownComponent([
+ '- list with inline $a=2$ inline formula $a + b < c$\n',
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it('renders math formula with greater-than-operator > in it', async () => {
- vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b > c$\n', '\n']);
+ wrapper = buildMarkdownComponent([
+ '- list with inline $a=2$ inline formula $a + b > c$\n',
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
});
});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 8bf049235a9..585cbb68eeb 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -1,12 +1,15 @@
import { mount } from '@vue/test-utils';
import json from 'test_fixtures/blob/notebook/basic.json';
import Output from '~/notebook/cells/output/index.vue';
+import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
+import { relativeRawPath, markdownCellContent } from '../../mock_data';
describe('Output component', () => {
let wrapper;
const createComponent = (output) => {
wrapper = mount(Output, {
+ provide: { relativeRawPath },
propsData: {
outputs: [].concat(output),
count: 1,
@@ -95,6 +98,17 @@ describe('Output component', () => {
});
});
+ describe('Markdown output', () => {
+ beforeEach(() => {
+ const markdownType = { data: { 'text/markdown': markdownCellContent } };
+ createComponent(markdownType);
+ });
+
+ it('renders a markdown component', () => {
+ expect(wrapper.findComponent(MarkdownOutput).props('rawCode')).toBe(markdownCellContent);
+ });
+ });
+
describe('default to plain text', () => {
beforeEach(() => {
const unknownType = json.cells[6];
diff --git a/spec/frontend/notebook/cells/output/markdown_spec.js b/spec/frontend/notebook/cells/output/markdown_spec.js
new file mode 100644
index 00000000000..e3490ed3bea
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/markdown_spec.js
@@ -0,0 +1,44 @@
+import { mount } from '@vue/test-utils';
+import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
+import Markdown from '~/notebook/cells/markdown.vue';
+import { relativeRawPath, markdownCellContent } from '../../mock_data';
+
+describe('markdown output cell', () => {
+ let wrapper;
+
+ const createComponent = ({ count = 0, index = 0 } = {}) => {
+ wrapper = mount(MarkdownOutput, {
+ provide: { relativeRawPath },
+ propsData: {
+ rawCode: markdownCellContent,
+ count,
+ index,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findPrompt = () => wrapper.findComponent(Prompt);
+ const findMarkdown = () => wrapper.findComponent(Markdown);
+
+ it.each`
+ index | count | showOutput
+ ${0} | ${1} | ${true}
+ ${1} | ${2} | ${false}
+ ${2} | ${3} | ${false}
+ `('renders a prompt', ({ index, count, showOutput }) => {
+ createComponent({ count, index });
+ expect(findPrompt().props()).toMatchObject({ count, showOutput, type: 'Out' });
+ });
+
+ it('renders a Markdown component', () => {
+ expect(findMarkdown().props()).toMatchObject({
+ cell: { source: markdownCellContent },
+ hidePrompt: true,
+ });
+ });
+});
diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js
new file mode 100644
index 00000000000..b1419e1256f
--- /dev/null
+++ b/spec/frontend/notebook/mock_data.js
@@ -0,0 +1,2 @@
+export const relativeRawPath = '/test';
+export const markdownCellContent = ['# Test'];
diff --git a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
index bc29903d4bf..a4611149432 100644
--- a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
+++ b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
@@ -2,7 +2,7 @@
exports[`note_app when sort direction is asc shows skeleton notes after the loaded discussions 1`] = `
"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
- <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\"></noteable-discussion-stub>
+ <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\" shouldscrolltonote=\\"true\\"></noteable-discussion-stub>
<skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
<!---->
</ul>"
@@ -11,7 +11,7 @@ exports[`note_app when sort direction is asc shows skeleton notes after the load
exports[`note_app when sort direction is desc shows skeleton notes before the loaded discussions 1`] = `
"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
<skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
- <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\"></noteable-discussion-stub>
+ <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\" shouldscrolltonote=\\"true\\"></noteable-discussion-stub>
<!---->
</ul>"
`;
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index cbe11c20798..c7420ca9c48 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -5,6 +5,8 @@ import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import noteActions from '~/notes/components/note_actions.vue';
+import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
+import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
import createStore from '~/notes/stores';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { userDataMock } from '../mock_data';
@@ -18,6 +20,23 @@ describe('noteActions', () => {
const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx);
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
+ const findTimelineButton = () => wrapper.findComponent(TimelineEventButton);
+
+ const setupStoreForIncidentTimelineEvents = ({
+ userCanAdd,
+ noteableType,
+ isPromotionInProgress = true,
+ }) => {
+ store.dispatch('setUserData', {
+ ...userDataMock,
+ can_add_timeline_events: userCanAdd,
+ });
+ store.state.noteableData = {
+ ...store.state.noteableData,
+ type: noteableType,
+ };
+ store.state.isPromoteCommentToTimelineEventInProgress = isPromotionInProgress;
+ };
const mountNoteActions = (propsData, computed) => {
return mount(noteActions, {
@@ -238,7 +257,8 @@ describe('noteActions', () => {
describe('user is not logged in', () => {
beforeEach(() => {
- store.dispatch('setUserData', {});
+ // userData can be null https://gitlab.com/gitlab-org/gitlab/-/issues/379375
+ store.dispatch('setUserData', null);
wrapper = mountNoteActions({
...props,
canDelete: false,
@@ -301,4 +321,56 @@ describe('noteActions', () => {
expect(resolveButton.attributes('title')).toBe('Thread stays unresolved');
});
});
+
+ describe('timeline event button', () => {
+ // why: We are working with an integrated store, so let's imply the getter is used
+ describe.each`
+ desc | userCanAdd | noteableType | exists
+ ${'default'} | ${true} | ${NOTEABLE_TYPE_MAPPING.Incident} | ${true}
+ ${'when cannot add incident timeline event'} | ${false} | ${NOTEABLE_TYPE_MAPPING.Incident} | ${false}
+ ${'when is not incident'} | ${true} | ${NOTEABLE_TYPE_MAPPING.MergeRequest} | ${false}
+ `('$desc', ({ userCanAdd, noteableType, exists }) => {
+ beforeEach(() => {
+ setupStoreForIncidentTimelineEvents({
+ userCanAdd,
+ noteableType,
+ });
+
+ wrapper = mountNoteActions({ ...props });
+ });
+
+ it(`handles rendering of timeline button (exists=${exists})`, () => {
+ expect(findTimelineButton().exists()).toBe(exists);
+ });
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ setupStoreForIncidentTimelineEvents({
+ userCanAdd: true,
+ noteableType: NOTEABLE_TYPE_MAPPING.Incident,
+ });
+
+ wrapper = mountNoteActions({ ...props });
+ });
+
+ it('should render timeline-event-button', () => {
+ expect(findTimelineButton().props()).toEqual({
+ noteId: props.noteId,
+ isPromotionInProgress: true,
+ });
+ });
+
+ it('when timeline-event-button emits click-promote-comment-to-event, dispatches action', () => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+
+ findTimelineButton().vm.$emit('click-promote-comment-to-event');
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenCalledWith('promoteCommentToTimelineEvent');
+ });
+ });
+ });
});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index b870cda2a24..56c22b09e1b 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -18,7 +18,7 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.findComponent({ ref: 'actionText' });
const findTimestampLink = () => wrapper.findComponent({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.findComponent({ ref: 'noteTimestamp' });
- const findInternalNoteIndicator = () => wrapper.findByTestId('internalNoteIndicator');
+ const findInternalNoteIndicator = () => wrapper.findByTestId('internal-note-indicator');
const findSpinner = () => wrapper.findComponent({ ref: 'spinner' });
const statusHtml =
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 45625d0a23f..81e4ed3ebe7 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -34,7 +34,7 @@ describe('Discussion navigation mixin', () => {
beforeEach(() => {
setHTMLFixture(
- `<div class="notes">
+ `<div class="tab-pane notes">
${[...'abcde']
.map(
(id) =>
diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js
index e03fa854e54..1514602d424 100644
--- a/spec/frontend/notes/stores/getters_spec.js
+++ b/spec/frontend/notes/stores/getters_spec.js
@@ -1,5 +1,5 @@
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
-import { DESC, ASC } from '~/notes/constants';
+import { DESC, ASC, NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
import * as getters from '~/notes/stores/getters';
import {
notesDataMock,
@@ -536,4 +536,24 @@ describe('Getters Notes Store', () => {
expect(getters.sortDirection(state)).toBe(DESC);
});
});
+
+ describe('canUserAddIncidentTimelineEvents', () => {
+ it.each`
+ userData | noteableData | expected
+ ${{ can_add_timeline_events: true }} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${true}
+ ${{ can_add_timeline_events: true }} | ${{ type: NOTEABLE_TYPE_MAPPING.Issue }} | ${false}
+ ${null} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${false}
+ ${{ can_add_timeline_events: false }} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${false}
+ `(
+ 'with userData=$userData and noteableData=$noteableData, expected=$expected',
+ ({ userData, noteableData, expected }) => {
+ Object.assign(state, {
+ userData,
+ noteableData,
+ });
+
+ expect(getters.canUserAddIncidentTimelineEvents(state)).toBe(expected);
+ },
+ );
+ });
});
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
new file mode 100644
index 00000000000..f0b318e69ec
--- /dev/null
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -0,0 +1,73 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ObservabilityApp from '~/observability/components/observability_app.vue';
+
+describe('Observability root app', () => {
+ let wrapper;
+ const replace = jest.fn();
+ const $router = {
+ replace,
+ };
+ const $route = {
+ pathname: 'https://gitlab.com/gitlab-org/',
+ query: { otherQuery: 100 },
+ };
+
+ const findIframe = () => wrapper.findByTestId('observability-ui-iframe');
+
+ const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
+
+ const mountComponent = (route = $route) => {
+ wrapper = shallowMountExtended(ObservabilityApp, {
+ propsData: {
+ observabilityIframeSrc: TEST_IFRAME_SRC,
+ },
+ mocks: {
+ $router,
+ $route: route,
+ },
+ });
+ };
+
+ 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);
+ });
+
+ 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();
+ });
+
+ 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 });
+ 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',
+ },
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index 0b59fe2d8ce..7da91c4af96 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -33,7 +33,7 @@ describe('Image List Row', () => {
const findListItemComponent = () => wrapper.findComponent(ListItem);
const findShowFullPathButton = () => wrapper.findComponent(GlButton);
- const mountComponent = (props, features = {}) => {
+ const mountComponent = (props) => {
wrapper = shallowMount(Component, {
stubs: {
RouterLink,
@@ -47,9 +47,6 @@ describe('Image List Row', () => {
},
provide: {
config: {},
- glFeatures: {
- ...features,
- },
},
directives: {
GlTooltip: createMockDirective(),
@@ -88,23 +85,43 @@ describe('Image List Row', () => {
});
describe('image title and path', () => {
- it('contains a link to the details page', () => {
+ it('renders shortened name of image and contains a link to the details page', () => {
mountComponent();
const link = findDetailsLink();
- expect(link.text()).toBe(item.path);
- expect(findDetailsLink().props('to')).toMatchObject({
+ expect(link.text()).toBe('gitlab-test/rails-12009');
+
+ expect(link.props('to')).toMatchObject({
name: 'details',
params: {
id: getIdFromGraphQLId(item.id),
},
});
+
+ expect(findShowFullPathButton().exists()).toBe(true);
});
it('when the image has no name lists the path', () => {
mountComponent({ item: { ...item, name: '' } });
+ expect(findDetailsLink().text()).toBe('gitlab-test');
+ });
+
+ it('clicking on shortened name of image hides the button & shows full path', async () => {
+ mountComponent();
+
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ const mockFocusFn = jest.fn();
+ wrapper.vm.$refs.imageName.$el.focus = mockFocusFn;
+
+ await findShowFullPathButton().trigger('click');
+
+ expect(findShowFullPathButton().exists()).toBe(false);
expect(findDetailsLink().text()).toBe(item.path);
+ expect(mockFocusFn).toHaveBeenCalled();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_show_full_path', {
+ label: 'registry_image_list',
+ });
});
it('contains a clipboard button', () => {
@@ -149,35 +166,6 @@ describe('Image List Row', () => {
expect(findClipboardButton().attributes('disabled')).toBe('true');
});
});
-
- describe('when containerRegistryShowShortenedPath feature enabled', () => {
- let trackingSpy;
-
- beforeEach(() => {
- mountComponent({}, { containerRegistryShowShortenedPath: true });
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('renders shortened name of image', () => {
- expect(findShowFullPathButton().exists()).toBe(true);
- expect(findDetailsLink().text()).toBe('gitlab-test/rails-12009');
- });
-
- it('clicking on shortened name of image hides the button & shows full path', async () => {
- const btn = findShowFullPathButton();
- const mockFocusFn = jest.fn();
- wrapper.vm.$refs.imageName.$el.focus = mockFocusFn;
-
- await btn.trigger('click');
-
- expect(findShowFullPathButton().exists()).toBe(false);
- expect(findDetailsLink().text()).toBe(item.path);
- expect(mockFocusFn).toHaveBeenCalled();
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_show_full_path', {
- label: 'registry_image_list',
- });
- });
- });
});
describe('delete button', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index e6d81d4a28f..bcc8e41fce8 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -58,6 +58,12 @@ describe('registry_header', () => {
describe('sub header parts', () => {
describe('images count', () => {
+ it('does not exist', async () => {
+ await mountComponent({ imagesCount: 0 });
+
+ expect(findImagesCountSubHeader().exists()).toBe(false);
+ });
+
it('exists', async () => {
await mountComponent({ imagesCount: 1 });
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 ee6470a9df8..310398b01cf 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
@@ -26,6 +26,7 @@ import {
import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
+import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue';
import Tracking from '~/tracking';
@@ -34,6 +35,7 @@ import {
graphQLImageDetailsMock,
graphQLDeleteImageRepositoryTagsMock,
graphQLDeleteImageRepositoryTagImportingErrorMock,
+ graphQLProjectImageRepositoriesDetailsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
tagsMock,
@@ -64,6 +66,9 @@ describe('Details Page', () => {
const defaultConfig = {
noContainersImage: 'noContainersImage',
+ projectListUrl: 'projectListUrl',
+ groupListUrl: 'groupListUrl',
+ isGroupPage: false,
};
const cleanTags = tagsMock.map((t) => {
@@ -81,7 +86,8 @@ describe('Details Page', () => {
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()),
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)),
+ tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock())),
+ detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
options,
config = defaultConfig,
} = {}) => {
@@ -91,6 +97,7 @@ describe('Details Page', () => {
[getContainerRepositoryDetailsQuery, resolver],
[deleteContainerRepositoryTagsMutation, mutationResolver],
[getContainerRepositoryTagsQuery, tagsResolver],
+ [getContainerRepositoriesDetails, detailsResolver],
];
apolloProvider = createMockApollo(requestHandlers);
@@ -256,11 +263,13 @@ describe('Details Page', () => {
describe('confirmDelete event', () => {
let mutationResolver;
let tagsResolver;
+ let detailsResolver;
beforeEach(() => {
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock));
- mountComponent({ mutationResolver, tagsResolver });
+ tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
+ detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
+ mountComponent({ mutationResolver, tagsResolver, detailsResolver });
return waitForApolloRequestRender();
});
@@ -280,6 +289,7 @@ describe('Details Page', () => {
await waitForPromises();
expect(tagsResolver).toHaveBeenCalled();
+ expect(detailsResolver).toHaveBeenCalled();
});
});
@@ -298,6 +308,7 @@ describe('Details Page', () => {
await waitForPromises();
expect(tagsResolver).toHaveBeenCalled();
+ expect(detailsResolver).toHaveBeenCalled();
});
});
});
@@ -359,14 +370,16 @@ describe('Details Page', () => {
describe('importing repository error', () => {
let mutationResolver;
let tagsResolver;
+ let detailsResolver;
beforeEach(async () => {
mutationResolver = jest
.fn()
.mockResolvedValue(graphQLDeleteImageRepositoryTagImportingErrorMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock));
+ tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
+ detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
- mountComponent({ mutationResolver, tagsResolver });
+ mountComponent({ mutationResolver, tagsResolver, detailsResolver });
await waitForApolloRequestRender();
});
@@ -378,6 +391,7 @@ describe('Details Page', () => {
await waitForPromises();
expect(tagsResolver).toHaveBeenCalled();
+ expect(detailsResolver).toHaveBeenCalled();
const deleteAlert = findDeleteAlert();
expect(deleteAlert.exists()).toBe(true);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index fb5ee4e6884..0164d92ce34 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -1,12 +1,13 @@
-import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
+import { GlTable, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import { last } from 'lodash';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
import PackagesList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue';
import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants';
import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants';
import Tracking from '~/tracking';
@@ -22,7 +23,7 @@ describe('packages_list', () => {
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
const findPackageListPagination = () => wrapper.findComponent(GlPagination);
- const findPackageListDeleteModal = () => wrapper.findComponent(GlModal);
+ const findPackageListDeleteModal = () => wrapper.findComponent(DeletePackageModal);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
@@ -65,7 +66,7 @@ describe('packages_list', () => {
stubs: {
...stubChildren(PackagesList),
GlTable,
- GlModal,
+ DeletePackageModal,
},
...options,
});
@@ -109,52 +110,38 @@ describe('packages_list', () => {
expect(sorting.exists()).toBe(true);
});
- it('contains a modal component', () => {
- const sorting = findPackageListDeleteModal();
- expect(sorting.exists()).toBe(true);
+ it("doesn't contain a modal component", () => {
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
});
describe('when the user can destroy the package', () => {
- beforeEach(() => {
+ let itemToBeDeleted;
+
+ beforeEach(async () => {
mountComponent();
+ itemToBeDeleted = last(packageList);
+ await findPackagesListRow().vm.$emit('packageToDelete', itemToBeDeleted);
});
- it('setItemToBeDeleted sets itemToBeDeleted and open the modal', async () => {
- const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
- const item = last(wrapper.vm.list);
-
- findPackagesListRow().vm.$emit('packageToDelete', item);
-
- await nextTick();
- expect(wrapper.vm.itemToBeDeleted).toEqual(item);
- expect(mockModalShow).toHaveBeenCalled();
+ afterEach(() => {
+ itemToBeDeleted = null;
});
- it('deleteItemConfirmation resets itemToBeDeleted', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemConfirmation();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ it('passes itemToBeDeleted to the modal', () => {
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(itemToBeDeleted);
});
it('deleteItemConfirmation emit package:delete', async () => {
- const itemToBeDeleted = { id: 2 };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted });
- wrapper.vm.deleteItemConfirmation();
- await nextTick();
+ await findPackageListDeleteModal().vm.$emit('ok');
+
expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
});
- it('deleteItemCanceled resets itemToBeDeleted', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemCanceled();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
+ await findPackageListDeleteModal().vm.$emit(event);
+
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
new file mode 100644
index 00000000000..e0e26434680
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
@@ -0,0 +1,71 @@
+import { GlModal as RealGlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
+
+const GlModal = stubComponent(RealGlModal, {
+ methods: {
+ show: jest.fn(),
+ },
+});
+
+describe('DeleteModal', () => {
+ let wrapper;
+
+ const defaultItemsToBeDeleted = [
+ {
+ name: 'package 01',
+ },
+ {
+ name: 'package 02',
+ },
+ ];
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const mountComponent = ({ itemsToBeDeleted = defaultItemsToBeDeleted } = {}) => {
+ wrapper = shallowMountExtended(DeleteModal, {
+ propsData: {
+ itemsToBeDeleted,
+ },
+ stubs: {
+ GlModal,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('passes title prop', () => {
+ expect(findModal().props('title')).toMatchInterpolatedText('Delete packages');
+ });
+
+ it('passes actionPrimary prop', () => {
+ expect(findModal().props('actionPrimary')).toStrictEqual({
+ text: 'Permanently delete',
+ attributes: [{ variant: 'danger' }, { category: 'primary' }],
+ });
+ });
+
+ it('renders description', () => {
+ expect(findModal().text()).toContain(
+ 'You are about to delete 2 packages. This operation is irreversible.',
+ );
+ });
+
+ it('emits confirm when primary event is emitted', () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('confirm')).toHaveLength(1);
+ });
+
+ it('show calls gl-modal show', () => {
+ findModal().vm.show();
+
+ expect(GlModal.methods.show).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
new file mode 100644
index 00000000000..f0fa9592419
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -0,0 +1,152 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import { packageData } from '../../mock_data';
+
+describe('PackageVersionsList', () => {
+ let wrapper;
+
+ const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>empty message</div>' };
+ const packageList = [
+ packageData({
+ name: 'version 1',
+ }),
+ packageData({
+ id: `gid://gitlab/Packages::Package/112`,
+ name: 'version 2',
+ }),
+ ];
+
+ const uiElements = {
+ findLoader: () => wrapper.findComponent(PackagesListLoader),
+ findListPagination: () => wrapper.findComponent(GlKeysetPagination),
+ findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
+ findListRow: () => wrapper.findAllComponents(VersionRow),
+ };
+ const mountComponent = (props) => {
+ wrapper = shallowMountExtended(PackageVersionsList, {
+ propsData: {
+ versions: packageList,
+ pageInfo: {},
+ isLoading: false,
+ ...props,
+ },
+ slots: {
+ 'empty-state': EmptySlotStub,
+ },
+ });
+ };
+
+ describe('when list is loading', () => {
+ beforeEach(() => {
+ mountComponent({ isLoading: true, versions: [] });
+ });
+ it('displays loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(true);
+ });
+
+ it('does not display rows', () => {
+ expect(uiElements.findListRow().exists()).toBe(false);
+ });
+
+ it('does not display empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(false);
+ });
+
+ it('does not display pagination', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when list is loaded and has no data', () => {
+ beforeEach(() => {
+ mountComponent({ isLoading: false, versions: [] });
+ });
+
+ it('displays empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(true);
+ });
+
+ it('does not display loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(false);
+ });
+
+ it('does not display rows', () => {
+ expect(uiElements.findListRow().exists()).toBe(false);
+ });
+
+ it('does not display pagination', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when list is loaded with data', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('displays package version rows', () => {
+ expect(uiElements.findListRow().exists()).toEqual(true);
+ expect(uiElements.findListRow()).toHaveLength(packageList.length);
+ });
+
+ it('binds the correct props', () => {
+ expect(uiElements.findListRow().at(0).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageList[0]),
+ });
+
+ expect(uiElements.findListRow().at(1).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageList[1]),
+ });
+ });
+
+ describe('pagination display', () => {
+ it('does not display pagination if there is no previous or next page', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+
+ it('displays pagination if pageInfo.hasNextPage is true', async () => {
+ await wrapper.setProps({ pageInfo: { hasNextPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+
+ it('displays pagination if pageInfo.hasPreviousPage is true', async () => {
+ await wrapper.setProps({ pageInfo: { hasPreviousPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+
+ it('displays pagination if both pageInfo.hasNextPage and pageInfo.hasPreviousPage are true', async () => {
+ await wrapper.setProps({ pageInfo: { hasNextPage: true, hasPreviousPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+ });
+
+ it('does not display loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(false);
+ });
+
+ it('does not display empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(false);
+ });
+ });
+
+ describe('when user interacts with pagination', () => {
+ beforeEach(() => {
+ mountComponent({ pageInfo: { hasNextPage: true } });
+ });
+
+ it('emits prev-page event when paginator emits prev event', () => {
+ uiElements.findListPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toHaveLength(1);
+ });
+
+ it('emits next-page when paginator emits next event', () => {
+ uiElements.findListPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index 5be05ddf629..a7de751aadd 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -8,7 +8,14 @@ exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-py-3"
>
- <!---->
+ <div
+ class="gl-w-7 gl-display-flex gl-justify-content-start gl-pl-2"
+ >
+ <gl-form-checkbox-stub
+ class="gl-m-0"
+ id="2"
+ />
+ </div>
<div
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
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 b5a512b8806..913b4f5926f 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 { GlSprintf } from '@gitlab/ui';
+import { GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -40,9 +40,11 @@ describe('packages_list_row', () => {
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+ const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const mountComponent = ({
packageEntity = packageWithoutTags,
+ selected = false,
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
@@ -53,6 +55,7 @@ describe('packages_list_row', () => {
},
propsData: {
packageEntity,
+ selected,
},
directives: {
GlTooltip: createMockDirective(),
@@ -117,14 +120,13 @@ describe('packages_list_row', () => {
});
});
- it('emits the packageToDelete event when the delete button is clicked', async () => {
+ it('emits the delete event when the delete button is clicked', async () => {
mountComponent({ packageEntity: packageWithoutTags });
findDeleteDropdown().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('packageToDelete')).toHaveLength(1);
- expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
+ expect(wrapper.emitted('delete')).toHaveLength(1);
});
});
@@ -151,6 +153,39 @@ describe('packages_list_row', () => {
});
});
+ describe('left action template', () => {
+ it('does not render checkbox if not permitted', () => {
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, canDestroy: false },
+ });
+
+ expect(findBulkDeleteAction().exists()).toBe(false);
+ });
+
+ it('renders checkbox', () => {
+ mountComponent();
+
+ expect(findBulkDeleteAction().exists()).toBe(true);
+ expect(findBulkDeleteAction().attributes('checked')).toBeUndefined();
+ });
+
+ it('emits select when checked', () => {
+ mountComponent();
+
+ findBulkDeleteAction().vm.$emit('change');
+
+ expect(wrapper.emitted('select')).toHaveLength(1);
+ });
+
+ it('renders checkbox in selected state if selected', () => {
+ mountComponent({
+ selected: true,
+ });
+
+ expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ });
+ });
+
describe('secondary left info', () => {
it('has the package version', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 3e3607a361c..7cc5bea0f7a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -1,8 +1,10 @@
-import { GlAlert, GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
@@ -35,16 +37,11 @@ describe('packages_list', () => {
};
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
- const GlModalStub = {
- name: GlModal.name,
- template: '<div><slot></slot></div>',
- methods: { show: jest.fn() },
- };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
- const findPackageListPagination = () => wrapper.findComponent(GlKeysetPagination);
- const findPackageListDeleteModal = () => wrapper.findComponent(GlModalStub);
+ const findPackageListDeleteModal = () => wrapper.findComponent(DeletePackageModal);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
+ const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
@@ -55,8 +52,9 @@ describe('packages_list', () => {
...props,
},
stubs: {
- GlModal: GlModalStub,
+ DeletePackageModal,
GlSprintf,
+ RegistryList,
},
slots: {
'empty-state': EmptySlotStub,
@@ -64,10 +62,6 @@ describe('packages_list', () => {
});
};
- beforeEach(() => {
- GlModalStub.methods.show.mockReset();
- });
-
afterEach(() => {
wrapper.destroy();
});
@@ -81,12 +75,12 @@ describe('packages_list', () => {
expect(findPackagesListLoader().exists()).toBe(true);
});
- it('does not show the rows', () => {
- expect(findPackagesListRow().exists()).toBe(false);
+ it('does not show the registry list', () => {
+ expect(findRegistryList().exists()).toBe(false);
});
- it('does not show the pagination', () => {
- expect(findPackageListPagination().exists()).toBe(false);
+ it('does not show the rows', () => {
+ expect(findPackagesListRow().exists()).toBe(false);
});
});
@@ -99,22 +93,29 @@ describe('packages_list', () => {
expect(findPackagesListLoader().exists()).toBe(false);
});
+ it('shows the registry list', () => {
+ expect(findRegistryList().exists()).toBe(true);
+ });
+
+ it('shows the registry list with the right props', () => {
+ expect(findRegistryList().props()).toMatchObject({
+ title: '2 packages',
+ items: defaultProps.list,
+ pagination: defaultProps.pageInfo,
+ isLoading: false,
+ });
+ });
+
it('shows the rows', () => {
expect(findPackagesListRow().exists()).toBe(true);
});
});
describe('layout', () => {
- it('contains a pagination component', () => {
- mountComponent({ pageInfo: { hasPreviousPage: true } });
-
- expect(findPackageListPagination().exists()).toBe(true);
- });
-
- it('contains a modal component', () => {
+ it("doesn't contain a visible modal component", () => {
mountComponent();
- expect(findPackageListDeleteModal().exists()).toBe(true);
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
it('does not have an error alert displayed', () => {
@@ -125,31 +126,46 @@ describe('packages_list', () => {
});
describe('when the user can destroy the package', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mountComponent();
- findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
- return nextTick();
+ await findPackagesListRow().vm.$emit('delete', firstPackage);
});
- it('deleting a package opens the modal', () => {
- expect(findPackageListDeleteModal().text()).toContain(firstPackage.name);
+ it('passes itemToBeDeleted to the modal', () => {
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
});
- it('confirming on the modal emits package:delete', async () => {
- findPackageListDeleteModal().vm.$emit('ok');
-
- await nextTick();
+ it('emits package:delete when modal confirms', async () => {
+ await findPackageListDeleteModal().vm.$emit('ok');
expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
});
- it('closing the modal resets itemToBeDeleted', async () => {
- // triggering the v-model
- findPackageListDeleteModal().vm.$emit('input', false);
+ it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
+ await findPackageListDeleteModal().vm.$emit(event);
- await nextTick();
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
+ });
+ });
+
+ describe('when the user can bulk destroy packages', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('passes itemToBeDeleted to the modal when there is only one package', async () => {
+ await findRegistryList().vm.$emit('delete', [firstPackage]);
+
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
+ expect(wrapper.emitted('delete')).toBeUndefined();
+ });
+
+ it('emits delete when there is more than one package', () => {
+ const items = [firstPackage, secondPackage];
+ findRegistryList().vm.$emit('delete', items);
- expect(findPackageListDeleteModal().text()).not.toContain(firstPackage.name);
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0]).toEqual([items]);
});
});
@@ -196,15 +212,15 @@ describe('packages_list', () => {
});
it('emits prev-page events when the prev event is fired', () => {
- findPackageListPagination().vm.$emit('prev');
+ findRegistryList().vm.$emit('prev-page');
- expect(wrapper.emitted('prev-page')).toEqual([[]]);
+ expect(wrapper.emitted('prev-page')).toHaveLength(1);
});
it('emits next-page events when the next event is fired', () => {
- findPackageListPagination().vm.$emit('next');
+ findRegistryList().vm.$emit('next-page');
- expect(wrapper.emitted('next-page')).toEqual([[]]);
+ expect(wrapper.emitted('next-page')).toHaveLength(1);
});
});
@@ -215,7 +231,7 @@ describe('packages_list', () => {
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
- findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
+ findPackagesListRow().vm.$emit('delete', firstPackage);
return nextTick();
});
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 c2b6fb734d6..f36c5923532 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -233,6 +233,12 @@ export const packageDetailsQuery = (extendPackage) => ({
},
versions: {
nodes: packageVersions(),
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
__typename: 'PackageConnection',
},
dependencyLinks: {
@@ -288,6 +294,33 @@ export const packageDestroyMutation = () => ({
},
});
+export const packagesDestroyMutation = () => ({
+ data: {
+ destroyPackages: {
+ errors: [],
+ },
+ },
+});
+
+export const packagesDestroyMutationError = () => ({
+ data: {
+ destroyPackages: null,
+ },
+ errors: [
+ {
+ message:
+ "The resource that you are attempting to access does not exist or you don't have permission to perform this action",
+ locations: [
+ {
+ line: 2,
+ column: 3,
+ },
+ ],
+ path: ['destroyPackages'],
+ },
+ ],
+});
+
export const packageDestroyMutationError = () => ({
data: {
destroyPackage: null,
@@ -314,6 +347,7 @@ export const packageDestroyFilesMutation = () => ({
},
},
});
+
export const packageDestroyFilesMutationError = () => ({
data: {
destroyPackageFiles: null,
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
index 17905a8db2d..c2fecf87428 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
@@ -2,12 +2,62 @@
exports[`PackagesListApp renders 1`] = `
<div>
+ <!---->
+
+ <gl-card-stub
+ bodyclass="gl-display-flex gl-p-0!"
+ class="gl-px-8 gl-py-6 gl-line-height-20 gl-mt-3"
+ footerclass=""
+ headerclass=""
+ >
+ <!---->
+
+ <div
+ class="gl-banner-content"
+ >
+ <h2
+ class="gl-banner-title"
+ >
+ Help us learn about your registry migration needs
+ </h2>
+
+ <p>
+ If you are interested in migrating packages from your private registry to the GitLab Package Registry, take our survey and tell us more about your needs.
+ </p>
+
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ data-testid="gl-banner-primary-button"
+ href="https://gitlab.fra1.qualtrics.com/jfe/form/SV_cHomH9FPzOaiDTU"
+ icon=""
+ size="medium"
+ variant="confirm"
+ >
+ Take survey
+ </gl-button-stub>
+
+ </div>
+
+ <gl-button-stub
+ aria-label="Close banner"
+ buttontextclasses=""
+ category="tertiary"
+ class="gl-banner-close"
+ icon="close"
+ size="small"
+ variant="default"
+ />
+ </gl-card-stub>
+
<package-title-stub
count="2"
helpurl="/help/user/packages/index"
/>
- <package-search-stub />
+ <package-search-stub
+ class="gl-mb-5"
+ />
<div>
<section
@@ -69,5 +119,7 @@ exports[`PackagesListApp renders 1`] = `
</div>
</section>
</div>
+
+ <div />
</div>
`;
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 a32e76a132e..f942a334f40 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
@@ -15,8 +15,8 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
-import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
@@ -99,6 +99,7 @@ describe('PackagesApp', () => {
GlSprintf,
GlTabs,
GlTab,
+ PackageVersionsList,
},
mocks: {
$route: {
@@ -120,8 +121,7 @@ describe('PackagesApp', () => {
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
- const findVersionRows = () => wrapper.findAllComponents(VersionRow);
- const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
+ const findVersionsList = () => wrapper.findComponent(PackageVersionsList);
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
@@ -558,38 +558,23 @@ describe('PackagesApp', () => {
});
describe('versions', () => {
- it('displays the correct version count when the package has versions', async () => {
+ it('displays versions list when the package has versions', async () => {
createComponent();
await waitForPromises();
- expect(findVersionRows()).toHaveLength(packageVersions().length);
+ expect(findVersionsList()).toBeDefined();
});
it('binds the correct props', async () => {
- const [versionPackage] = packageVersions();
- // eslint-disable-next-line no-underscore-dangle
- delete versionPackage.__typename;
- delete versionPackage.tags;
-
- createComponent();
-
+ const versionNodes = packageVersions();
+ createComponent({ packageEntity: { versions: { nodes: versionNodes } } });
await waitForPromises();
- expect(findVersionRows().at(0).props()).toMatchObject({
- packageEntity: expect.objectContaining(versionPackage),
+ expect(findVersionsList().props()).toMatchObject({
+ versions: expect.arrayContaining(versionNodes),
});
});
-
- it('displays the no versions message when there are none', async () => {
- createComponent({
- resolver: jest.fn().mockResolvedValue(packageDetailsQuery({ versions: { nodes: [] } })),
- });
-
- await waitForPromises();
-
- expect(noVersionsMessage().exists()).toBe(true);
- });
});
describe('dependency links', () => {
it('does not show the dependency links for a non nuget package', async () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index 0e74fbbc6d9..abdb875e839 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -1,30 +1,39 @@
-import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlAlert, GlBanner, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-
+import * as utils from '~/lib/utils/common_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import ListPage from '~/packages_and_registries/package_registry/pages/list.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
-
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
+ HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
+ DELETE_PACKAGES_ERROR_MESSAGE,
+ DELETE_PACKAGES_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
+import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
+import {
+ packagesListQuery,
+ packageData,
+ pagination,
+ packagesDestroyMutation,
+ packagesDestroyMutationError,
+} from '../mock_data';
-import { packagesListQuery, packageData, pagination } from '../mock_data';
-
-jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
describe('PackagesListApp', () => {
@@ -49,31 +58,44 @@ describe('PackagesListApp', () => {
filters: { packageName: 'foo', packageType: 'CONAN' },
};
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findBanner = () => wrapper.findComponent(GlBanner);
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDeletePackage = () => wrapper.findComponent(DeletePackage);
+ const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
+ mutationResolver,
provide = defaultProvide,
} = {}) => {
Vue.use(VueApollo);
- const requestHandlers = [[getPackagesQuery, resolver]];
+ const requestHandlers = [
+ [getPackagesQuery, resolver],
+ [destroyPackagesMutation, mutationResolver],
+ ];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(ListPage, {
apolloProvider,
provide,
stubs: {
+ GlBanner,
GlEmptyState,
GlLoadingIcon,
GlSprintf,
GlLink,
PackageList,
DeletePackage,
+ DeleteModal: stubComponent(DeleteModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
},
});
};
@@ -116,6 +138,70 @@ describe('PackagesListApp', () => {
});
});
+ describe('package migration survey banner', () => {
+ describe('with no cookie set', () => {
+ beforeEach(() => {
+ utils.setCookie = jest.fn();
+
+ mountComponent();
+ });
+
+ it('displays the banner', () => {
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('does not call setCookie', () => {
+ expect(utils.setCookie).not.toHaveBeenCalled();
+ });
+
+ describe('when the close button is clicked', () => {
+ beforeEach(() => {
+ findBanner().vm.$emit('close');
+ });
+
+ it('sets the dismissed cookie', () => {
+ expect(utils.setCookie).toHaveBeenCalledWith(
+ HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
+ 'true',
+ );
+ });
+
+ it('does not display the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('when the primary button is clicked', () => {
+ beforeEach(() => {
+ findBanner().vm.$emit('primary');
+ });
+
+ it('sets the dismissed cookie', () => {
+ expect(utils.setCookie).toHaveBeenCalledWith(
+ HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
+ 'true',
+ );
+ });
+
+ it('does not display the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with the dismissed cookie set', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getCookie').mockReturnValue('true');
+
+ mountComponent();
+ });
+
+ it('does not display the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+ });
+
describe('search component', () => {
it('exists', () => {
mountComponent();
@@ -282,4 +368,62 @@ describe('PackagesListApp', () => {
expect(findListComponent().props('isLoading')).toBe(false);
});
});
+
+ describe('bulk delete package', () => {
+ const items = [{ id: '1' }, { id: '2' }];
+
+ it('deletePackage is bound to package-list package:delete event', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('delete', [{ id: '1' }, { id: '2' }]);
+
+ await waitForPromises();
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual(items);
+ });
+
+ it('calls mutation with the right values and shows success alert', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
+ mountComponent({
+ mutationResolver,
+ });
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('delete', items);
+
+ findDeletePackagesModal().vm.$emit('confirm');
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ ids: items.map((item) => item.id),
+ });
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().props('variant')).toEqual('success');
+ expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_SUCCESS_MESSAGE);
+ });
+
+ it('on error shows danger alert', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutationError());
+ mountComponent({
+ mutationResolver,
+ });
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('delete', items);
+
+ findDeletePackagesModal().vm.$emit('confirm');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().props('variant')).toEqual('danger');
+ expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_ERROR_MESSAGE);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
new file mode 100644
index 00000000000..8f229182fe5
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
@@ -0,0 +1,78 @@
+import { GlFormGroup, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import component from '~/packages_and_registries/settings/group/components/forwarding_settings.vue';
+
+describe('Forwarding Settings', () => {
+ let wrapper;
+
+ const defaultProps = {
+ disabled: false,
+ forwarding: false,
+ label: 'label',
+ lockForwarding: false,
+ modelNames: {
+ forwarding: 'forwardField',
+ lockForwarding: 'lockForwardingField',
+ isLocked: 'lockedField',
+ },
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findForwardingCheckbox = () => wrapper.findByTestId('forwarding-checkbox');
+ const findLockForwardingCheckbox = () => wrapper.findByTestId('lock-forwarding-checkbox');
+
+ it('has a form group', () => {
+ mountComponent();
+
+ expect(findFormGroup().exists()).toBe(true);
+ expect(findFormGroup().attributes()).toMatchObject({
+ label: defaultProps.label,
+ });
+ });
+
+ describe.each`
+ name | finder | label | extraProps | field
+ ${'forwarding'} | ${findForwardingCheckbox} | ${'Forward label package requests'} | ${{ forwarding: true }} | ${defaultProps.modelNames.forwarding}
+ ${'lock forwarding'} | ${findLockForwardingCheckbox} | ${'Enforce label setting for all subgroups'} | ${{ lockForwarding: true }} | ${defaultProps.modelNames.lockForwarding}
+ `('$name checkbox', ({ name, finder, label, extraProps, field }) => {
+ it('is rendered', () => {
+ mountComponent();
+ expect(finder().exists()).toBe(true);
+ expect(finder().text()).toMatchInterpolatedText(label);
+ expect(finder().attributes('disabled')).toBeUndefined();
+ expect(finder().attributes('checked')).toBeUndefined();
+ });
+
+ it(`is checked when ${name} set`, () => {
+ mountComponent({ ...defaultProps, ...extraProps });
+
+ expect(finder().attributes('checked')).toBe('true');
+ });
+
+ it(`emits an update event with field ${field} set`, () => {
+ mountComponent();
+
+ finder().vm.$emit('change', true);
+
+ expect(wrapper.emitted('update')).toStrictEqual([[field, true]]);
+ });
+ });
+
+ describe('disabled', () => {
+ it('disables both checkboxes', () => {
+ mountComponent({ ...defaultProps, disabled: true });
+
+ expect(findForwardingCheckbox().attributes('disabled')).toEqual('true');
+ expect(findLockForwardingCheckbox().attributes('disabled')).toEqual('true');
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 31fc3ad419c..7edc321867c 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
+import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
@@ -60,6 +61,7 @@ describe('Group Settings App', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findPackageSettings = () => wrapper.findComponent(PackagesSettings);
+ const findPackageForwardingSettings = () => wrapper.findComponent(PackagesForwardingSettings);
const findDependencyProxySettings = () => wrapper.findComponent(DependencyProxySettings);
const waitForApolloQueryAndRender = async () => {
@@ -67,16 +69,18 @@ describe('Group Settings App', () => {
await nextTick();
};
- const packageSettingsProps = { packageSettings: packageSettings() };
+ const packageSettingsProps = { packageSettings };
+ const packageForwardingSettingsProps = { forwardSettings: { ...packageSettings } };
const dependencyProxyProps = {
dependencyProxySettings: dependencyProxySettings(),
dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(),
};
describe.each`
- finder | entitySpecificProps | successMessage | errorMessage
- ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
- ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'}
+ finder | entitySpecificProps | successMessage | errorMessage
+ ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
+ ${findPackageForwardingSettings} | ${packageForwardingSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
+ ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'}
`('settings blocks', ({ finder, entitySpecificProps, successMessage, errorMessage }) => {
beforeEach(() => {
mountComponent();
@@ -88,10 +92,7 @@ describe('Group Settings App', () => {
});
it('binds the correctProps', () => {
- expect(finder().props()).toMatchObject({
- isLoading: false,
- ...entitySpecificProps,
- });
+ expect(finder().props()).toMatchObject(entitySpecificProps);
});
describe('success event', () => {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 13eba39ec8c..807f332f4d3 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -48,7 +48,7 @@ describe('Packages Settings', () => {
apolloProvider,
provide: defaultProvide,
propsData: {
- packageSettings: packageSettings(),
+ packageSettings,
},
stubs: {
SettingsBlock,
@@ -83,7 +83,7 @@ describe('Packages Settings', () => {
};
const emitMavenSettingsUpdate = (override) => {
- findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', {
+ findMavenDuplicatedSettingsExceptionsInput().vm.$emit('update', {
mavenDuplicateExceptionRegex: ')',
...override,
});
@@ -117,7 +117,7 @@ describe('Packages Settings', () => {
it('renders toggle', () => {
mountComponent({ mountFn: mountExtended });
- const { mavenDuplicatesAllowed } = packageSettings();
+ const { mavenDuplicatesAllowed } = packageSettings;
expect(findMavenDuplicatedSettingsToggle().exists()).toBe(true);
@@ -132,7 +132,7 @@ describe('Packages Settings', () => {
it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
mountComponent({ mountFn: mountExtended });
- const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings();
+ const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings;
expect(findMavenDuplicatedSettingsExceptionsInput().exists()).toBe(true);
@@ -170,7 +170,7 @@ describe('Packages Settings', () => {
it('renders toggle', () => {
mountComponent({ mountFn: mountExtended });
- const { genericDuplicatesAllowed } = packageSettings();
+ const { genericDuplicatesAllowed } = packageSettings;
expect(findGenericDuplicatedSettingsToggle().exists()).toBe(true);
expect(findGenericDuplicatedSettingsToggle().props()).toMatchObject({
@@ -184,7 +184,7 @@ describe('Packages Settings', () => {
it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => {
mountComponent({ mountFn: mountExtended });
- const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings();
+ const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings;
expect(findGenericDuplicatedSettingsExceptionsInput().props()).toMatchObject({
duplicatesAllowed: genericDuplicatesAllowed,
@@ -239,7 +239,7 @@ describe('Packages Settings', () => {
emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex });
expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
- ...packageSettings(),
+ ...packageSettings,
mavenDuplicateExceptionRegex,
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
new file mode 100644
index 00000000000..a0b257a9496
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
@@ -0,0 +1,280 @@
+import Vue from 'vue';
+import { GlButton } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import component from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
+import {
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ PACKAGE_FORWARDING_SETTINGS_HEADER,
+} from '~/packages_and_registries/settings/group/constants';
+
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ packageSettings,
+ packageForwardingSettings,
+ groupPackageSettingsMock,
+ groupPackageForwardSettingsMutationMock,
+ mutationErrorMock,
+ npmProps,
+ pypiProps,
+ mavenProps,
+} from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
+
+describe('Packages Forwarding Settings', () => {
+ let wrapper;
+ let apolloProvider;
+ const mutationResolverFn = jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock());
+
+ const defaultProvide = {
+ groupPath: 'foo_group_path',
+ };
+
+ const mountComponent = ({
+ forwardSettings = { ...packageSettings },
+ features = {},
+ mutationResolver = mutationResolverFn,
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[updateNamespacePackageSettings, mutationResolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(component, {
+ apolloProvider,
+ provide: {
+ ...defaultProvide,
+ glFeatures: {
+ ...features,
+ },
+ },
+ propsData: {
+ forwardSettings,
+ },
+ stubs: {
+ SettingsBlock,
+ },
+ });
+ };
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findForm = () => wrapper.find('form');
+ const findSubmitButton = () => findForm().findComponent(GlButton);
+ const findDescription = () => wrapper.findByTestId('description');
+ const findMavenForwardingSettings = () => wrapper.findByTestId('maven');
+ const findNpmForwardingSettings = () => wrapper.findByTestId('npm');
+ const findPyPiForwardingSettings = () => wrapper.findByTestId('pypi');
+
+ const fillApolloCache = () => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getGroupPackagesSettingsQuery,
+ variables: {
+ fullPath: defaultProvide.groupPath,
+ },
+ ...groupPackageSettingsMock,
+ });
+ };
+
+ const updateNpmSettings = () => {
+ findNpmForwardingSettings().vm.$emit('update', 'npmPackageRequestsForwarding', false);
+ };
+
+ const submitForm = () => {
+ findForm().trigger('submit');
+ return waitForPromises();
+ };
+
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(PACKAGE_FORWARDING_SETTINGS_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ );
+ });
+
+ it('watches changes to props', async () => {
+ mountComponent();
+
+ expect(findNpmForwardingSettings().props()).toMatchObject(npmProps);
+
+ await wrapper.setProps({
+ forwardSettings: {
+ ...packageSettings,
+ npmPackageRequestsForwardingLocked: true,
+ },
+ });
+
+ expect(findNpmForwardingSettings().props()).toMatchObject({ ...npmProps, disabled: true });
+ });
+
+ it('submit button is disabled', () => {
+ mountComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ });
+
+ describe.each`
+ type | finder | props | field
+ ${'npm'} | ${findNpmForwardingSettings} | ${npmProps} | ${'npmPackageRequestsForwarding'}
+ ${'pypi'} | ${findPyPiForwardingSettings} | ${pypiProps} | ${'pypiPackageRequestsForwarding'}
+ ${'maven'} | ${findMavenForwardingSettings} | ${mavenProps} | ${'mavenPackageRequestsForwarding'}
+ `('$type settings', ({ finder, props, field }) => {
+ beforeEach(() => {
+ mountComponent({ features: { mavenCentralRequestForwarding: true } });
+ });
+
+ it('assigns forwarding settings props', () => {
+ expect(finder().props()).toMatchObject(props);
+ });
+
+ it('on update event enables submit button', async () => {
+ finder().vm.$emit('update', field, false);
+
+ await waitForPromises();
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('maven settings', () => {
+ describe('with feature turned off', () => {
+ it('does not exist', () => {
+ mountComponent();
+
+ expect(findMavenForwardingSettings().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('settings update', () => {
+ describe('success state', () => {
+ it('calls the mutation with the right variables', async () => {
+ const {
+ mavenPackageRequestsForwardingLocked,
+ npmPackageRequestsForwardingLocked,
+ pypiPackageRequestsForwardingLocked,
+ ...packageSettingsInput
+ } = packageForwardingSettings;
+
+ mountComponent();
+
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(mutationResolverFn).toHaveBeenCalledWith({
+ input: {
+ namespacePath: defaultProvide.groupPath,
+ ...packageSettingsInput,
+ npmPackageRequestsForwarding: false,
+ },
+ });
+ });
+
+ it('when field are locked calls the mutation with the right variables', async () => {
+ mountComponent({
+ forwardSettings: {
+ ...packageSettings,
+ mavenPackageRequestsForwardingLocked: true,
+ pypiPackageRequestsForwardingLocked: true,
+ },
+ });
+
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(mutationResolverFn).toHaveBeenCalledWith({
+ input: {
+ namespacePath: defaultProvide.groupPath,
+ lockNpmPackageRequestsForwarding: false,
+ npmPackageRequestsForwarding: false,
+ },
+ });
+ });
+
+ it('emits a success event', async () => {
+ mountComponent();
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(wrapper.emitted('success')).toHaveLength(1);
+ });
+
+ it('has an optimistic response', async () => {
+ const npmPackageRequestsForwarding = false;
+ mountComponent();
+
+ fillApolloCache();
+
+ expect(findNpmForwardingSettings().props('forwarding')).toBe(true);
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
+ ...packageSettings,
+ npmPackageRequestsForwarding,
+ });
+ expect(findNpmForwardingSettings().props('forwarding')).toBe(npmPackageRequestsForwarding);
+ });
+ });
+
+ describe('errors', () => {
+ it('mutation payload with root level errors', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(mutationErrorMock);
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+
+ it.each`
+ type | mutationResolver
+ ${'local'} | ${jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock({ errors: ['foo'] }))}
+ ${'network'} | ${jest.fn().mockRejectedValue()}
+ `('mutation payload with $type error', async ({ mutationResolver }) => {
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index d53446de910..1ca9dc6daeb 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -1,9 +1,26 @@
-export const packageSettings = () => ({
+const packageDuplicateSettings = {
mavenDuplicatesAllowed: true,
mavenDuplicateExceptionRegex: '',
genericDuplicatesAllowed: true,
genericDuplicateExceptionRegex: '',
-});
+};
+
+export const packageForwardingSettings = {
+ mavenPackageRequestsForwarding: true,
+ lockMavenPackageRequestsForwarding: false,
+ npmPackageRequestsForwarding: true,
+ lockNpmPackageRequestsForwarding: false,
+ pypiPackageRequestsForwarding: true,
+ lockPypiPackageRequestsForwarding: false,
+ mavenPackageRequestsForwardingLocked: false,
+ npmPackageRequestsForwardingLocked: false,
+ pypiPackageRequestsForwardingLocked: false,
+};
+
+export const packageSettings = {
+ ...packageDuplicateSettings,
+ ...packageForwardingSettings,
+};
export const dependencyProxySettings = (extend) => ({
enabled: true,
@@ -21,13 +38,52 @@ export const groupPackageSettingsMock = {
group: {
id: '1',
fullPath: 'foo_group_path',
- packageSettings: packageSettings(),
+ packageSettings: {
+ ...packageSettings,
+ __typename: 'PackageSettings',
+ },
dependencyProxySetting: dependencyProxySettings(),
dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(),
},
},
};
+export const npmProps = {
+ forwarding: packageForwardingSettings.npmPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockNpmPackageRequestsForwarding,
+ label: 'npm',
+ disabled: false,
+ modelNames: {
+ forwarding: 'npmPackageRequestsForwarding',
+ lockForwarding: 'lockNpmPackageRequestsForwarding',
+ isLocked: 'npmPackageRequestsForwardingLocked',
+ },
+};
+
+export const pypiProps = {
+ forwarding: packageForwardingSettings.pypiPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockPypiPackageRequestsForwarding,
+ label: 'PyPI',
+ disabled: false,
+ modelNames: {
+ forwarding: 'pypiPackageRequestsForwarding',
+ lockForwarding: 'lockPypiPackageRequestsForwarding',
+ isLocked: 'pypiPackageRequestsForwardingLocked',
+ },
+};
+
+export const mavenProps = {
+ forwarding: packageForwardingSettings.mavenPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockMavenPackageRequestsForwarding,
+ label: 'Maven',
+ disabled: false,
+ modelNames: {
+ forwarding: 'mavenPackageRequestsForwarding',
+ lockForwarding: 'lockMavenPackageRequestsForwarding',
+ isLocked: 'mavenPackageRequestsForwardingLocked',
+ },
+};
+
export const groupPackageSettingsMutationMock = (override) => ({
data: {
updateNamespacePackageSettings: {
@@ -43,6 +99,19 @@ export const groupPackageSettingsMutationMock = (override) => ({
},
});
+export const groupPackageForwardSettingsMutationMock = (override) => ({
+ data: {
+ updateNamespacePackageSettings: {
+ packageSettings: {
+ npmPackageRequestsForwarding: true,
+ lockNpmPackageRequestsForwarding: false,
+ },
+ errors: [],
+ ...override,
+ },
+ },
+});
+
export const dependencyProxySettingMutationMock = (override) => ({
data: {
updateDependencyProxySettings: {
diff --git a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
new file mode 100644
index 00000000000..357dab593e8
--- /dev/null
+++ b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
@@ -0,0 +1,82 @@
+import { GlSprintf, GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
+
+describe('DeletePackageModal', () => {
+ let wrapper;
+
+ const defaultItemToBeDeleted = {
+ name: 'package 01',
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const mountComponent = ({ itemToBeDeleted = defaultItemToBeDeleted } = {}) => {
+ wrapper = shallowMountExtended(DeletePackageModal, {
+ propsData: {
+ itemToBeDeleted,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when itemToBeDeleted prop is defined', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('displays modal', () => {
+ expect(findModal().props('visible')).toBe(true);
+ });
+
+ it('passes title prop', () => {
+ expect(findModal().props('title')).toBe(wrapper.vm.$options.i18n.modalTitle);
+ });
+
+ it('passes actionPrimary prop', () => {
+ expect(findModal().props('actionPrimary')).toStrictEqual({
+ text: wrapper.vm.$options.i18n.modalAction,
+ attributes: {
+ variant: 'danger',
+ },
+ });
+ });
+
+ it('displays description', () => {
+ const descriptionEl = findModal().findComponent(GlSprintf);
+
+ expect(descriptionEl.exists()).toBe(true);
+ expect(descriptionEl.attributes('message')).toBe(wrapper.vm.$options.i18n.modalDescription);
+ });
+
+ it('emits ok when modal is validate', () => {
+ expect(wrapper.emitted().ok).toBeUndefined();
+
+ findModal().vm.$emit('ok');
+
+ expect(wrapper.emitted().ok).toHaveLength(1);
+ });
+
+ it('emits cancel when modal close', () => {
+ expect(wrapper.emitted().cancel).toBeUndefined();
+
+ findModal().vm.$emit('change', false);
+
+ expect(wrapper.emitted().cancel).toHaveLength(1);
+ });
+ });
+
+ describe('when itemToBeDeleted prop is null', () => {
+ beforeEach(() => {
+ mountComponent({ itemToBeDeleted: null });
+ });
+
+ it("doesn't display modal", () => {
+ expect(findModal().props('visible')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
index aab78c99190..6b6833b00c3 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
@@ -147,6 +147,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="set_up_your_first_project_s_ci_cd"
@@ -171,6 +172,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="start_a_free_trial_of_gitlab_ultimate"
@@ -196,6 +198,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="add_code_owners"
@@ -228,6 +231,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="enable_require_merge_approvals"
@@ -294,6 +298,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="create_an_issue"
@@ -318,6 +323,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="submit_a_merge_request_mr"
@@ -376,6 +382,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="run_a_security_scan_using_ci_cd"
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index f54d56c3af4..4cac642bb50 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -1,139 +1,7 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { formatUtcOffset, formatTimezone } from '~/lib/utils/datetime_utility';
-import TimezoneDropdown, {
- findTimezoneByIdentifier,
-} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
+import { findTimezoneByIdentifier } from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
describe('Timezone Dropdown', () => {
- let $inputEl = null;
- let $dropdownEl = null;
- let $wrapper = null;
- const tzListSel = '.dropdown-content ul li a.is-active';
-
- const initTimezoneDropdown = (options = {}) => {
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- ...options,
- });
- };
-
- const findDropdownToggleText = () => $wrapper.find('.dropdown-toggle-text');
-
- describe('Initialize', () => {
- describe('with dropdown already loaded', () => {
- beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
- $wrapper = $('.dropdown');
- $inputEl = $('#schedule_cron_timezone');
- $inputEl.val('');
- $dropdownEl = $('.js-timezone-dropdown');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('can take an $inputEl in the constructor', () => {
- initTimezoneDropdown();
-
- const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
- const tzValue = 'Asia/Colombo';
-
- expect($inputEl.val()).toBe('Etc/UTC');
-
- $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click');
-
- const val = $inputEl.val();
-
- expect(val).toBe(tzValue);
- expect(val).not.toBe('Etc/UTC');
- });
-
- it('will format data array of timezones into a list of offsets', () => {
- initTimezoneDropdown();
-
- const data = $dropdownEl.data('data');
- const formatted = $wrapper.find(tzListSel).text();
-
- data.forEach((item) => {
- expect(formatted).toContain(formatTimezone(item));
- });
- });
-
- describe('when `allowEmpty` property is `false`', () => {
- beforeEach(() => {
- initTimezoneDropdown();
- });
-
- it('will default the timezone to UTC', () => {
- const tz = $inputEl.val();
-
- expect(tz).toBe('Etc/UTC');
- });
- });
-
- describe('when `allowEmpty` property is `true`', () => {
- beforeEach(() => {
- initTimezoneDropdown({
- allowEmpty: true,
- });
- });
-
- it('will default the value of the input to an empty string', () => {
- expect($inputEl.val()).toBe('');
- });
- });
- });
-
- describe('without dropdown loaded', () => {
- beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
- $wrapper = $('.dropdown');
- $inputEl = $('#schedule_cron_timezone');
- $dropdownEl = $('.js-timezone-dropdown');
- });
-
- it('will populate the list of UTC offsets after the dropdown is loaded', () => {
- expect($wrapper.find(tzListSel).length).toEqual(0);
-
- initTimezoneDropdown();
-
- expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length);
- });
-
- it('will call a provided handler when a new timezone is selected', () => {
- const onSelectTimezone = jest.fn();
-
- initTimezoneDropdown({ onSelectTimezone });
-
- $wrapper.find(tzListSel).first().trigger('click');
-
- expect(onSelectTimezone).toHaveBeenCalled();
- });
-
- it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => {
- $inputEl.val('America/St_Johns');
-
- initTimezoneDropdown({ displayFormat: (selectedItem) => formatTimezone(selectedItem) });
-
- expect(findDropdownToggleText().html()).toEqual('[UTC - 2.5] Newfoundland');
- });
-
- it('will call a provided `displayFormat` handler to format the dropdown value', () => {
- const displayFormat = jest.fn();
-
- initTimezoneDropdown({ displayFormat });
-
- $wrapper.find(tzListSel).first().trigger('click');
-
- expect(displayFormat).toHaveBeenCalled();
- });
- });
- });
-
describe('formatUtcOffset', () => {
it('will convert negative utc offsets in seconds to hours and minutes', () => {
expect(formatUtcOffset(-21600)).toEqual('- 6');
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 ed7d4ad269e..b202a148306 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
@@ -137,6 +137,8 @@ describe('Settings Panel', () => {
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findFeatureFlagsSettings = () => wrapper.findComponent({ ref: 'feature-flags-settings' });
+ const findInfrastructureSettings = () =>
+ wrapper.findComponent({ ref: 'infrastructure-settings' });
const findReleasesSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' });
@@ -841,6 +843,24 @@ describe('Settings Panel', () => {
});
});
});
+ describe('Infrastructure', () => {
+ describe('with feature flag', () => {
+ it('should show the infrastructure toggle', () => {
+ wrapper = mountComponent({
+ glFeatures: { splitOperationsVisibilityPermissions: true },
+ });
+
+ expect(findInfrastructureSettings().exists()).toBe(true);
+ });
+ });
+ describe('without feature flag', () => {
+ it('should not show the infrastructure toggle', () => {
+ wrapper = mountComponent({});
+
+ expect(findInfrastructureSettings().exists()).toBe(false);
+ });
+ });
+ });
describe('Releases', () => {
describe('with feature flag', () => {
it('should show the releases toggle', () => {
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 0f947e84e0f..67d0fbdd9d1 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -116,7 +116,7 @@ describe('WikiForm', () => {
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
markdownDocsPath: pageInfoPersisted.markdownHelpPath,
uploadsPath: pageInfoPersisted.uploadsPath,
- initOnAutofocus: pageInfoPersisted.persisted,
+ autofocus: pageInfoPersisted.persisted,
formFieldId: 'wiki_content',
formFieldName: 'wiki[content]',
}),
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 3b79739630d..27707f8b01a 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -253,7 +253,7 @@ describe('Pipeline editor tabs component', () => {
appStatus | editor | viz | validate | merged
${undefined} | ${true} | ${true} | ${true} | ${true}
${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${true} | ${false}
- ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false}
+ ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${true}
${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true}
`(
'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged',
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
deleted file mode 100644
index cce8f480928..00000000000
--- a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-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 PipelineSchedules from '~/pipeline_schedules/components/pipeline_schedules.vue';
-import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue';
-import deletePipelineScheduleMutation from '~/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql';
-import getPipelineSchedulesQuery from '~/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
-import {
- mockGetPipelineSchedulesGraphQLResponse,
- mockPipelineScheduleNodes,
- deleteMutationResponse,
-} from '../mock_data';
-
-Vue.use(VueApollo);
-
-describe('Pipeline schedules app', () => {
- let wrapper;
-
- const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
- const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
-
- const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
- const deleteMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
-
- const createMockApolloProvider = (
- requestHandlers = [[getPipelineSchedulesQuery, successHandler]],
- ) => {
- return createMockApollo(requestHandlers);
- };
-
- const createComponent = (requestHandlers) => {
- wrapper = shallowMount(PipelineSchedules, {
- provide: {
- fullPath: 'gitlab-org/gitlab',
- },
- apolloProvider: createMockApolloProvider(requestHandlers),
- });
- };
-
- const findTable = () => wrapper.findComponent(PipelineSchedulesTable);
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findModal = () => wrapper.findComponent(GlModal);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays table', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findTable().exists()).toBe(true);
- expect(findAlert().exists()).toBe(false);
- });
-
- it('fetches query and passes an array of pipeline schedules', async () => {
- createComponent();
-
- expect(successHandler).toHaveBeenCalled();
-
- await waitForPromises();
-
- expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
- });
-
- it('handles loading state', async () => {
- createComponent();
-
- expect(findLoadingIcon().exists()).toBe(true);
-
- await waitForPromises();
-
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('shows query error alert', async () => {
- createComponent([[getPipelineSchedulesQuery, failedHandler]]);
-
- await waitForPromises();
-
- expect(findAlert().text()).toBe('There was a problem fetching pipeline schedules.');
- });
-
- it('shows delete mutation error alert', async () => {
- createComponent([
- [getPipelineSchedulesQuery, successHandler],
- [deletePipelineScheduleMutation, deleteMutationHandlerFailed],
- ]);
-
- await waitForPromises();
-
- findModal().vm.$emit('primary');
-
- await waitForPromises();
-
- expect(findAlert().text()).toBe('There was a problem deleting the pipeline schedule.');
- });
-
- it('deletes pipeline schedule and refetches query', async () => {
- createComponent([
- [getPipelineSchedulesQuery, successHandler],
- [deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
- ]);
-
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
-
- await waitForPromises();
-
- const scheduleId = mockPipelineScheduleNodes[0].id;
-
- findTable().vm.$emit('showDeleteModal', scheduleId);
-
- expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
-
- findModal().vm.$emit('primary');
-
- await waitForPromises();
-
- expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
- id: scheduleId,
- });
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
- });
-
- it('modal should be visible after event', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findModal().props('visible')).toBe(false);
-
- findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
-
- await nextTick();
-
- expect(findModal().props('visible')).toBe(true);
- });
-
- it('modal should be hidden', async () => {
- createComponent();
-
- await waitForPromises();
-
- findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
-
- await nextTick();
-
- expect(findModal().props('visible')).toBe(true);
-
- findModal().vm.$emit('hide');
-
- await nextTick();
-
- expect(findModal().props('visible')).toBe(false);
- });
-});
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index 3680d9d62c7..c2cb95d4320 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -2,10 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue';
-import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
-import Dag from '~/pipelines/components/dag/dag.vue';
-import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
-import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
describe('The Pipeline Tabs', () => {
let wrapper;
@@ -16,12 +12,6 @@ describe('The Pipeline Tabs', () => {
const findPipelineTab = () => wrapper.findByTestId('pipeline-tab');
const findTestsTab = () => wrapper.findByTestId('tests-tab');
- const findDagApp = () => wrapper.findComponent(Dag);
- const findFailedJobsApp = () => wrapper.findComponent(JobsApp);
- const findJobsApp = () => wrapper.findComponent(JobsApp);
- const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
- const findTestsApp = () => wrapper.findComponent(TestReports);
-
const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter');
const findJobsBadge = () => wrapper.findByTestId('builds-counter');
const findTestsBadge = () => wrapper.findByTestId('tests-counter');
@@ -43,6 +33,7 @@ describe('The Pipeline Tabs', () => {
},
stubs: {
GlTab,
+ RouterView: true,
},
}),
);
@@ -54,17 +45,16 @@ describe('The Pipeline Tabs', () => {
describe('Tabs', () => {
it.each`
- tabName | tabComponent | appComponent
- ${'Pipeline'} | ${findPipelineTab} | ${findPipelineApp}
- ${'Dag'} | ${findDagTab} | ${findDagApp}
- ${'Jobs'} | ${findJobsTab} | ${findJobsApp}
- ${'Failed Jobs'} | ${findFailedJobsTab} | ${findFailedJobsApp}
- ${'Tests'} | ${findTestsTab} | ${findTestsApp}
- `('shows $tabName tab with its associated component', ({ appComponent, tabComponent }) => {
+ tabName | tabComponent
+ ${'Pipeline'} | ${findPipelineTab}
+ ${'Dag'} | ${findDagTab}
+ ${'Jobs'} | ${findJobsTab}
+ ${'Failed Jobs'} | ${findFailedJobsTab}
+ ${'Tests'} | ${findTestsTab}
+ `('shows $tabName tab', ({ tabComponent }) => {
createComponent();
expect(tabComponent().exists()).toBe(true);
- expect(appComponent().exists()).toBe(true);
});
describe('with no failed jobs', () => {
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 57d1511d859..36bce65dd56 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -730,6 +730,7 @@ export const mockPipelineTag = () => {
},
active: false,
source: 'push',
+ name: 'Build pipeline',
created_at: '2022-02-02T15:39:04.012Z',
updated_at: '2022-02-02T15:40:59.573Z',
path: '/root/mr-widgets/-/pipelines/311',
@@ -835,6 +836,7 @@ export const mockPipelineTag = () => {
],
duration: 93,
finished_at: '2022-02-02T15:40:59.384Z',
+ event_type_name: 'Pipeline',
name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
@@ -954,6 +956,7 @@ export const mockPipelineBranch = () => {
},
active: false,
source: 'push',
+ name: 'Build pipeline',
created_at: '2022-01-14T17:40:27.866Z',
updated_at: '2022-01-14T18:02:35.850Z',
path: '/root/mr-widgets/-/pipelines/268',
@@ -1041,6 +1044,7 @@ export const mockPipelineBranch = () => {
],
duration: 75,
finished_at: '2022-01-14T18:02:35.842Z',
+ event_type_name: 'Pipeline',
name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
index d6b13da3c3a..41b020189d0 100644
--- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
@@ -1,5 +1,5 @@
import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils';
-import { TAB_QUERY_PARAM, validPipelineTabNames } from '~/pipelines/constants';
+import { validPipelineTabNames } from '~/pipelines/constants';
describe('utils functions', () => {
const jobName1 = 'build_1';
@@ -173,18 +173,25 @@ describe('utils functions', () => {
describe('getPipelineDefaultTab', () => {
const baseUrl = 'http://gitlab.com/user/multi-projects-small/-/pipelines/332/';
- it('returns null if there was no `tab` params', () => {
+ it('returns null if there is only the base url', () => {
expect(getPipelineDefaultTab(baseUrl)).toBe(null);
});
- it('returns null if there was no valid tab param', () => {
- expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=invalid`)).toBe(null);
+ it('returns null if there was no valid last url part', () => {
+ expect(getPipelineDefaultTab(`${baseUrl}something`)).toBe(null);
});
it('returns the correct tab name if present', () => {
validPipelineTabNames.forEach((tabName) => {
- expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=${tabName}`)).toBe(tabName);
+ expect(getPipelineDefaultTab(`${baseUrl}${tabName}`)).toBe(tabName);
});
});
+
+ it('returns the right value even with query params', () => {
+ const [tabName] = validPipelineTabNames;
+ expect(getPipelineDefaultTab(`${baseUrl}${tabName}?query="something"&query2="else"`)).toBe(
+ tabName,
+ );
+ });
});
});
diff --git a/spec/frontend/pipelines/pipeline_tabs_spec.js b/spec/frontend/pipelines/pipeline_tabs_spec.js
index b184ce31d20..099748a5cca 100644
--- a/spec/frontend/pipelines/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/pipeline_tabs_spec.js
@@ -1,9 +1,7 @@
-import { createAppOptions, createPipelineTabs } from '~/pipelines/pipeline_tabs';
-import { updateHistory } from '~/lib/utils/url_utility';
+import { createAppOptions } from '~/pipelines/pipeline_tabs';
jest.mock('~/lib/utils/url_utility', () => ({
removeParams: () => 'gitlab.com',
- updateHistory: jest.fn(),
joinPaths: () => {},
setUrlFragment: () => {},
}));
@@ -64,32 +62,4 @@ describe('~/pipelines/pipeline_tabs.js', () => {
expect(createAppOptions('foo', null)).toBe(null);
});
});
-
- describe('createPipelineTabs', () => {
- const title = 'Pipeline Tabs';
-
- beforeAll(() => {
- document.title = title;
- });
-
- afterAll(() => {
- document.title = '';
- });
-
- it('calls `updateHistory` with correct params', () => {
- createPipelineTabs({});
-
- expect(updateHistory).toHaveBeenCalledWith({
- title,
- url: 'gitlab.com',
- replace: true,
- });
- });
-
- it("returns early if options aren't provided", () => {
- createPipelineTabs();
-
- expect(updateHistory).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 1d66607e72b..c62898f0c83 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,3 +1,4 @@
+import { merge } from 'lodash';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
@@ -20,6 +21,7 @@ describe('Pipeline Url Component', () => {
const findCommitRefName = () => wrapper.findByTestId('commit-ref-name');
const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
+ const findPipelineNameContainer = () => wrapper.findByTestId('pipeline-name-container');
const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]');
const defaultProps = mockPipeline(projectPath);
@@ -51,7 +53,16 @@ describe('Pipeline Url Component', () => {
expect(findPipelineUrlLink().text()).toBe('#1');
});
- it('should render the commit title, commit reference and commit-short-sha', () => {
+ it('should render the pipeline name instead of commit title', () => {
+ createComponent(merge(mockPipeline(projectPath), { pipeline: { name: 'Build pipeline' } }));
+
+ expect(findCommitTitleContainer().exists()).toBe(false);
+ expect(findPipelineNameContainer().exists()).toBe(true);
+ expect(findRefName().exists()).toBe(true);
+ expect(findCommitShortSha().exists()).toBe(true);
+ });
+
+ it('should render the commit title when pipeline has no name', () => {
createComponent();
const commitWrapper = findCommitTitleContainer();
@@ -59,6 +70,7 @@ describe('Pipeline Url Component', () => {
expect(findCommitTitle(commitWrapper).exists()).toBe(true);
expect(findRefName().exists()).toBe(true);
expect(findCommitShortSha().exists()).toBe(true);
+ expect(findPipelineNameContainer().exists()).toBe(false);
});
describe('commit user avatar', () => {
@@ -142,7 +154,7 @@ describe('Pipeline Url Component', () => {
});
it('tracks commit title click', () => {
- createComponent(mockPipelineBranch());
+ createComponent(merge(mockPipelineBranch(), { pipeline: { name: null } }));
findCommitTitle(findCommitTitleContainer()).vm.$emit('click');
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index 26e61efc4f6..a70ef10aa7b 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -13,11 +13,7 @@ import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
jest.mock('~/flash');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('Pipelines Actions dropdown', () => {
let wrapper;
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index 736d149f06d..974650a2c7c 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -19,6 +19,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<gl-button-stub
buttontextclasses=""
category="primary"
+ data-qa-selector="delete_button"
icon=""
role="button"
size="medium"
@@ -102,6 +103,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
</p>
<gl-form-input-stub
+ data-qa-selector="confirm_name_field"
id="confirm_name_input"
name="confirm_name_input"
type="text"
diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
index 26495fbcf83..ac020fe6915 100644
--- a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
@@ -20,6 +20,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
<gl-button-stub
buttontextclasses=""
category="primary"
+ data-qa-selector="delete_button"
icon=""
role="button"
size="medium"
@@ -103,6 +104,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
</p>
<gl-form-input-stub
+ data-qa-selector="confirm_name_field"
id="confirm_name_input"
name="confirm_name_input"
type="text"
diff --git a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
index f50dd393174..16b4493c622 100644
--- a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
+++ b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
@@ -71,7 +71,7 @@ describe('New project push tip popover', () => {
it('displays a link to open the push command help page reference', () => {
expect(findHelpLink().attributes().href).toBe(
- `${workingWithProjectsHelpPath}#push-to-create-a-new-project`,
+ `${workingWithProjectsHelpPath}#create-a-new-project-with-git-push`,
);
});
});
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index e3aaf760d1e..d8876349c5e 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -8,6 +8,12 @@ import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/u
import Component from '~/projects/pipelines/charts/components/app.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import API from '~/api';
+import { mockTracking } from 'helpers/tracking_helper';
+import {
+ SNOWPLOW_DATA_SOURCE,
+ SNOWPLOW_LABEL,
+ SNOWPLOW_SCHEMA,
+} from '~/projects/pipelines/charts/constants';
jest.mock('~/lib/utils/url_utility');
@@ -125,21 +131,59 @@ describe('ProjectsPipelinesChartsApp', () => {
});
describe('event tracking', () => {
- it.each`
- testId | event
- ${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
- ${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
- ${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
- ${'time-to-restore-service-tab'} | ${'p_analytics_ci_cd_time_to_restore_service'}
- ${'change-failure-rate-tab'} | ${'p_analytics_ci_cd_change_failure_rate'}
- `('tracks the $event event when clicked', ({ testId, event }) => {
- jest.spyOn(API, 'trackRedisHllUserEvent');
+ describe('RedisHLL events', () => {
+ it.each`
+ testId | event
+ ${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
+ ${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
+ ${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
+ ${'time-to-restore-service-tab'} | ${'p_analytics_ci_cd_time_to_restore_service'}
+ ${'change-failure-rate-tab'} | ${'p_analytics_ci_cd_change_failure_rate'}
+ `('tracks the $event event when clicked', ({ testId, event }) => {
+ const trackApiSpy = jest.spyOn(API, 'trackRedisHllUserEvent');
+
+ expect(trackApiSpy).not.toHaveBeenCalled();
+
+ wrapper.findByTestId(testId).vm.$emit('click');
+
+ expect(trackApiSpy).toHaveBeenCalledWith(event);
+ });
+ });
- expect(API.trackRedisHllUserEvent).not.toHaveBeenCalled();
+ describe('Snowplow events', () => {
+ it.each`
+ testId | event
+ ${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
+ ${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
+ ${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
+ `('tracks the $event event when clicked', ({ testId, event }) => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ wrapper.findByTestId(testId).vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', {
+ label: SNOWPLOW_LABEL,
+ context: {
+ schema: SNOWPLOW_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: SNOWPLOW_DATA_SOURCE,
+ },
+ },
+ });
+ });
- wrapper.findByTestId(testId).vm.$emit('click');
+ it.each`
+ tab
+ ${'time-to-restore-service-tab'}
+ ${'change-failure-rate-tab'}
+ `('does not track when tab $tab is clicked', ({ tab }) => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event);
+ wrapper.findByTestId(tab).vm.$emit('click');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
});
});
});
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 bf4026b65db..27065a704e2 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
@@ -12,7 +12,11 @@ import {
import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { sprintf } from '~/locale';
-import { branchProtectionsMockResponse } from './mock_data';
+import {
+ branchProtectionsMockResponse,
+ approvalRulesMock,
+ statusChecksRulesMock,
+} from './mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
@@ -34,6 +38,7 @@ describe('View branch rules', () => {
const projectPath = 'test/testing';
const protectedBranchesPath = 'protected/branches';
const approvalRulesPath = 'approval/rules';
+ const statusChecksPath = 'status/checks';
const branchProtectionsMockRequestHandler = jest
.fn()
.mockResolvedValue(branchProtectionsMockResponse);
@@ -43,7 +48,7 @@ describe('View branch rules', () => {
wrapper = shallowMountExtended(RuleView, {
apolloProvider: fakeApollo,
- provide: { projectPath, protectedBranchesPath, approvalRulesPath },
+ provide: { projectPath, protectedBranchesPath, approvalRulesPath, statusChecksPath },
});
await waitForPromises();
@@ -59,6 +64,7 @@ describe('View branch rules', () => {
const findBranchProtections = () => wrapper.findAllComponents(Protection);
const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription);
const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
+ const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
it('gets the branch param from url and renders it in the view', () => {
expect(util.getParameterByName).toHaveBeenCalledWith('branch');
@@ -105,9 +111,21 @@ describe('View branch rules', () => {
expect(findApprovalsTitle().exists()).toBe(true);
expect(findBranchProtections().at(2).props()).toMatchObject({
- header: sprintf(I18N.approvalsHeader, { total: 0 }),
+ header: sprintf(I18N.approvalsHeader, { total: 3 }),
headerLinkHref: approvalRulesPath,
headerLinkTitle: I18N.manageApprovalsLinkTitle,
+ approvals: approvalRulesMock,
+ });
+ });
+
+ it('renders a branch protection component for status checks', () => {
+ expect(findStatusChecksTitle().exists()).toBe(true);
+
+ expect(findBranchProtections().at(3).props()).toMatchObject({
+ header: sprintf(I18N.statusChecksHeader, { total: 2 }),
+ headerLinkHref: statusChecksPath,
+ headerLinkTitle: I18N.statusChecksLinkTitle,
+ statusChecks: statusChecksRulesMock,
});
});
});
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 c3f573061da..c07d4673344 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
@@ -1,29 +1,34 @@
const usersMock = [
{
+ id: '123',
username: 'usr1',
webUrl: 'http://test.test/usr1',
name: 'User 1',
avatarUrl: 'http://test.test/avt1.png',
},
{
+ id: '456',
username: 'usr2',
webUrl: 'http://test.test/usr2',
name: 'User 2',
avatarUrl: 'http://test.test/avt2.png',
},
{
+ id: '789',
username: 'usr3',
webUrl: 'http://test.test/usr3',
name: 'User 3',
avatarUrl: 'http://test.test/avt3.png',
},
{
+ id: '987',
username: 'usr4',
webUrl: 'http://test.test/usr4',
name: 'User 4',
avatarUrl: 'http://test.test/avt4.png',
},
{
+ id: '654',
username: 'usr5',
webUrl: 'http://test.test/usr5',
name: 'User 5',
@@ -40,6 +45,22 @@ const approvalsRequired = 3;
const groupsMock = [{ name: 'test_group_1' }, { name: 'test_group_2' }];
+export const approvalRulesMock = [
+ {
+ __typename: 'ApprovalProjectRule',
+ id: '123',
+ name: 'test',
+ type: 'REGULAR',
+ eligibleApprovers: { nodes: usersMock },
+ approvalsRequired,
+ },
+];
+
+export const statusChecksRulesMock = [
+ { __typename: 'StatusCheckRule', id: '123', name: 'test', externalUrl: 'https://test.test' },
+ { __typename: 'StatusCheckRule', id: '456', name: 'test 2', externalUrl: 'https://test2.test2' },
+];
+
export const protectionPropsMock = {
header: 'Test protection',
headerLinkTitle: 'Test link title',
@@ -47,13 +68,8 @@ export const protectionPropsMock = {
roles: accessLevelsMock,
users: usersMock,
groups: groupsMock,
- approvals: [
- {
- name: 'test',
- eligibleApprovers: { nodes: usersMock },
- approvalsRequired,
- },
- ],
+ approvals: approvalRulesMock,
+ statusChecks: statusChecksRulesMock,
};
export const protectionRowPropsMock = {
@@ -61,6 +77,7 @@ export const protectionRowPropsMock = {
users: usersMock,
accessLevels: accessLevelsMock,
approvalsRequired,
+ statusCheckUrl: statusChecksRulesMock[0].externalUrl,
};
export const accessLevelsMockResponse = [
@@ -116,6 +133,14 @@ export const branchProtectionsMockResponse = {
edges: accessLevelsMockResponse,
},
},
+ approvalRules: {
+ __typename: 'ApprovalProjectRuleConnection',
+ nodes: approvalRulesMock,
+ },
+ externalStatusChecks: {
+ __typename: 'ExternalStatusCheckConnection',
+ nodes: statusChecksRulesMock,
+ },
},
{
__typename: 'BranchRule',
@@ -133,6 +158,14 @@ export const branchProtectionsMockResponse = {
edges: [],
},
},
+ approvalRules: {
+ __typename: 'ApprovalProjectRuleConnection',
+ nodes: [],
+ },
+ externalStatusChecks: {
+ __typename: 'ExternalStatusCheckConnection',
+ nodes: [],
+ },
},
],
},
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
index b0a69bedd3e..a98b156f94e 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
@@ -27,6 +27,7 @@ describe('Branch rule protection row', () => {
const findAccessLevels = () => wrapper.findAllByTestId('access-level');
const findApprovalsRequired = () =>
wrapper.findByText(`${protectionRowPropsMock.approvalsRequired} approvals required`);
+ const findStatusChecksUrl = () => wrapper.findByText(protectionRowPropsMock.statusCheckUrl);
it('renders a title', () => {
expect(findTitle().exists()).toBe(true);
@@ -68,4 +69,8 @@ describe('Branch rule protection row', () => {
it('renders the number of approvals required', () => {
expect(findApprovalsRequired().exists()).toBe(true);
});
+
+ it('renders status checks URL', () => {
+ expect(findStatusChecksUrl().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
index e2fbb4f5bbb..caf967b4257 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
@@ -65,4 +65,15 @@ describe('Branch rule protection', () => {
approvalsRequired: approval.approvalsRequired,
});
});
+
+ it('renders a protection row for status checks', () => {
+ const statusCheck = protectionPropsMock.statusChecks[0];
+ expect(findProtectionRows().at(4).props()).toMatchObject({
+ title: statusCheck.name,
+ showDivider: false,
+ statusCheckUrl: statusCheck.externalUrl,
+ });
+
+ expect(findProtectionRows().at(5).props('showDivider')).toBe(true);
+ });
});
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index 6e639f895a8..e091f3e25c3 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,18 +1,9 @@
-import Vue, { nextTick } from 'vue';
-import { GlAlert } from '@gitlab/ui';
-import VueApollo from 'vue-apollo';
-import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
-import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
import { getTransferLocations } from '~/api/projects_api';
-import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/api/projects_api', () => ({
getTransferLocations: jest.fn(),
@@ -21,68 +12,36 @@ jest.mock('~/api/projects_api', () => ({
describe('Transfer project form', () => {
let wrapper;
- const projectId = '1';
+ const resourceId = '1';
const confirmButtonText = 'Confirm';
const confirmationPhrase = 'You must construct additional pylons!';
- Vue.use(VueApollo);
-
- const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
- const mockResolvedGetTransferLocations = ({
- data = transferLocationsResponsePage1,
- page = '1',
- nextPage = '2',
- prevPage = null,
- } = {}) => {
- getTransferLocations.mockResolvedValueOnce({
- data,
- headers: {
- 'x-per-page': '2',
- 'x-page': page,
- 'x-total': '4',
- 'x-total-pages': '2',
- 'x-next-page': nextPage,
- 'x-prev-page': prevPage,
- },
- });
- };
- const mockRejectedGetTransferLocations = () => {
- const error = new Error();
-
- getTransferLocations.mockRejectedValueOnce(error);
- };
-
- const createComponent = ({
- requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
- } = {}) => {
+ const createComponent = () => {
wrapper = shallowMountExtended(TransferProjectForm, {
provide: {
- projectId,
+ resourceId,
},
propsData: {
confirmButtonText,
confirmationPhrase,
},
- apolloProvider: createMockApollo(requestHandlers),
});
};
- const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
- const showNamespaceSelect = async () => {
- findNamespaceSelect().vm.$emit('show');
- await waitForPromises();
- };
+ const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
});
- it('renders the namespace selector', () => {
+ it('renders the namespace selector and passes `groupTransferLocationsApiMethod` prop', () => {
createComponent();
- expect(findNamespaceSelect().exists()).toBe(true);
+ expect(findTransferLocations().exists()).toBe(true);
+
+ findTransferLocations().props('groupTransferLocationsApiMethod')();
+ expect(getTransferLocations).toHaveBeenCalled();
});
it('renders the confirm button', () => {
@@ -100,220 +59,29 @@ describe('Transfer project form', () => {
describe('with a selected namespace', () => {
const [selectedItem] = transferLocationsResponsePage1;
- const arrange = async () => {
- mockResolvedGetTransferLocations();
+ beforeEach(() => {
createComponent();
- await showNamespaceSelect();
- findNamespaceSelect().vm.$emit('select', selectedItem);
- };
+ findTransferLocations().vm.$emit('input', selectedItem);
+ });
- it('emits the `selectNamespace` event when a namespace is selected', async () => {
- await arrange();
+ it('sets `value` prop on `TransferLocations` component', () => {
+ expect(findTransferLocations().props('value')).toEqual(selectedItem);
+ });
+ it('emits the `selectTransferLocation` event when a namespace is selected', async () => {
const args = [selectedItem.id];
- expect(wrapper.emitted('selectNamespace')).toEqual([args]);
+ expect(wrapper.emitted('selectTransferLocation')).toEqual([args]);
});
it('enables the confirm button', async () => {
- await arrange();
-
expect(findConfirmDanger().attributes('disabled')).toBeUndefined();
});
it('clicking the confirm button emits the `confirm` event', async () => {
- await arrange();
-
findConfirmDanger().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toBeDefined();
});
});
-
- describe('when `NamespaceSelect` is opened', () => {
- it('fetches user and group namespaces and passes correct props to `NamespaceSelect` component', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
-
- expect(findNamespaceSelect().props()).toMatchObject({
- userNamespaces: [
- {
- id: getIdFromGraphQLId(namespace.id),
- humanName: namespace.fullName,
- },
- ],
- groupNamespaces: transferLocationsResponsePage1.map(({ id, full_name: humanName }) => ({
- id,
- humanName,
- })),
- hasNextPageOfGroups: true,
- isLoading: false,
- isSearchLoading: false,
- shouldFilterNamespaces: false,
- });
- });
-
- describe('when namespaces have already been fetched', () => {
- beforeEach(async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- });
-
- it('does not fetch namespaces', async () => {
- getTransferLocations.mockClear();
- defaultQueryHandler.mockClear();
-
- await showNamespaceSelect();
-
- expect(getTransferLocations).not.toHaveBeenCalled();
- expect(defaultQueryHandler).not.toHaveBeenCalled();
- });
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays error alert', async () => {
- mockRejectedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
-
- describe('when `currentUser` GraphQL query fails', () => {
- it('displays error alert', async () => {
- mockResolvedGetTransferLocations();
- const error = new Error();
- createComponent({
- requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
- });
- await showNamespaceSelect();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
- });
-
- describe('when `search` event is fired', () => {
- const arrange = async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockResolvedGetTransferLocations();
- findNamespaceSelect().vm.$emit('search', 'foo');
- await nextTick();
- };
-
- it('sets `isSearchLoading` prop to `true`', async () => {
- await arrange();
-
- expect(findNamespaceSelect().props('isSearchLoading')).toBe(true);
- });
-
- it('passes `search` param to API call', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(getTransferLocations).toHaveBeenCalledWith(
- projectId,
- expect.objectContaining({ search: 'foo' }),
- );
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays dismissible error alert', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockRejectedGetTransferLocations();
- findNamespaceSelect().vm.$emit('search', 'foo');
- await waitForPromises();
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(true);
-
- alert.vm.$emit('dismiss');
- await nextTick();
-
- expect(alert.exists()).toBe(false);
- });
- });
- });
-
- describe('when `load-more-groups` event is fired', () => {
- const arrange = async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- mockResolvedGetTransferLocations({
- data: transferLocationsResponsePage2,
- page: '2',
- nextPage: null,
- prevPage: '1',
- });
-
- findNamespaceSelect().vm.$emit('load-more-groups');
- await nextTick();
- };
-
- it('sets `isLoading` prop to `true`', async () => {
- await arrange();
-
- expect(findNamespaceSelect().props('isLoading')).toBe(true);
- });
-
- it('passes `page` param to API call', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(getTransferLocations).toHaveBeenCalledWith(
- projectId,
- expect.objectContaining({ page: 2 }),
- );
- });
-
- it('updates `groupNamespaces` prop with new groups', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(findNamespaceSelect().props('groupNamespaces')).toMatchObject(
- [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
- ({ id, full_name: humanName }) => ({
- id,
- humanName,
- }),
- ),
- );
- });
-
- it('updates `hasNextPageOfGroups` prop', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false);
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays error alert', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockRejectedGetTransferLocations();
- findNamespaceSelect().vm.$emit('load-more-groups');
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
- });
});
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 4603436c40a..6369f04781f 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -52,6 +52,10 @@ describe('Branch rules app', () => {
expect(findAllBranchRules().at(0).props('name')).toBe(nodes[0].name);
+ expect(findAllBranchRules().at(0).props('branchProtection')).toEqual(nodes[0].branchProtection);
+
expect(findAllBranchRules().at(1).props('name')).toBe(nodes[1].name);
+
+ expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection);
});
});
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 2bc705f538b..2aa93fd0e28 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
@@ -2,7 +2,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BranchRule, {
i18n,
} from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
-import { branchRuleProvideMock, branchRulePropsMock } from '../mock_data';
+import { sprintf, n__ } from '~/locale';
+import {
+ branchRuleProvideMock,
+ branchRulePropsMock,
+ branchRuleWithoutDetailsPropsMock,
+} from '../mock_data';
describe('Branch rule', () => {
let wrapper;
@@ -15,7 +20,6 @@ describe('Branch rule', () => {
};
const findDefaultBadge = () => wrapper.findByText(i18n.defaultLabel);
- const findProtectedBadge = () => wrapper.findByText(i18n.protectedLabel);
const findBranchName = () => wrapper.findByText(branchRulePropsMock.name);
const findProtectionDetailsList = () => wrapper.findByRole('list');
const findProtectionDetailsListItems = () => wrapper.findAllByRole('listitem');
@@ -28,33 +32,36 @@ describe('Branch rule', () => {
});
describe('badges', () => {
- it('renders both default and protected badges', () => {
+ it('renders default badge', () => {
expect(findDefaultBadge().exists()).toBe(true);
- expect(findProtectedBadge().exists()).toBe(true);
});
it('does not render default badge if isDefault is set to false', () => {
createComponent({ isDefault: false });
expect(findDefaultBadge().exists()).toBe(false);
});
-
- it('does not render protected badge if isProtected is set to false', () => {
- createComponent({ isProtected: false });
- expect(findProtectedBadge().exists()).toBe(false);
- });
});
- it('does not render the protection details list of no details are present', () => {
- createComponent({ approvalDetails: null });
+ it('does not render the protection details list if no details are present', () => {
+ createComponent(branchRuleWithoutDetailsPropsMock);
expect(findProtectionDetailsList().exists()).toBe(false);
});
it('renders the protection details list items', () => {
- expect(findProtectionDetailsListItems().at(0).text()).toBe(
- branchRulePropsMock.approvalDetails[0],
+ 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(1).text()).toBe(
- branchRulePropsMock.approvalDetails[1],
+ expect(findProtectionDetailsListItems().at(3).text()).toMatchInterpolatedText(
+ sprintf(i18n.approvalRules, {
+ total: branchRulePropsMock.approvalRulesTotal,
+ subject: n__('rule', 'rules', branchRulePropsMock.approvalRulesTotal),
+ }),
);
});
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 bac82992c4d..8aa03a12996 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -8,10 +8,36 @@ export const branchRulesMockResponse = {
nodes: [
{
name: 'main',
+ isDefault: true,
+ branchProtection: {
+ allowForcePush: true,
+ codeOwnerApprovalRequired: true,
+ },
+ approvalRules: {
+ nodes: [{ id: 1 }],
+ __typename: 'ApprovalProjectRuleConnection',
+ },
+ externalStatusChecks: {
+ nodes: [{ id: 1 }, { id: 2 }],
+ __typename: 'BranchRule',
+ },
__typename: 'BranchRule',
},
{
name: 'test-*',
+ isDefault: false,
+ branchProtection: {
+ allowForcePush: false,
+ codeOwnerApprovalRequired: false,
+ },
+ approvalRules: {
+ nodes: [],
+ __typename: 'ApprovalProjectRuleConnection',
+ },
+ externalStatusChecks: {
+ nodes: [],
+ __typename: 'BranchRule',
+ },
__typename: 'BranchRule',
},
],
@@ -31,6 +57,21 @@ export const branchRuleProvideMock = {
export const branchRulePropsMock = {
name: 'main',
isDefault: true,
- isProtected: true,
- approvalDetails: ['requires approval from TEST', '2 status checks'],
+ branchProtection: {
+ allowForcePush: true,
+ codeOwnerApprovalRequired: true,
+ },
+ approvalRulesTotal: 1,
+ statusChecksTotal: 2,
+};
+
+export const branchRuleWithoutDetailsPropsMock = {
+ name: 'main',
+ isDefault: false,
+ branchProtection: {
+ allowForcePush: false,
+ codeOwnerApprovalRequired: false,
+ },
+ approvalRulesTotal: 0,
+ statusChecksTotal: 0,
};
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 7c3f4e76ae5..f9762491507 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -64,20 +64,14 @@ describe('ServiceDeskSetting', () => {
});
});
- describe('when customEmailEnabled', () => {
- beforeEach(() => {
- wrapper = createComponent({
- props: { customEmailEnabled: true },
- });
- });
+ describe('service desk email "from" name', () => {
+ it('service desk e-mail "from" name input appears', () => {
+ wrapper = createComponent();
- it('should not display help text', () => {
- expect(findSuffixFormGroup().text()).not.toContain(
- 'To add a custom suffix, set up a Service Desk email address',
- );
- expect(findSuffixFormGroup().text()).toContain(
- 'Add a suffix to Service Desk email address',
- );
+ const input = wrapper.findByTestId('email-from-name');
+
+ expect(input.exists()).toBe(true);
+ expect(input.attributes('disabled')).toBeUndefined();
});
});
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 1ff5766b074..b1e9d8d1256 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -292,6 +292,42 @@ describe('Release edit component', () => {
});
});
+ describe('remove button state', () => {
+ describe('when there is only one link', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: release.assets.links.slice(0, 1),
+ },
+ },
+ });
+ });
+
+ it('remove asset link button should not be present', () => {
+ expect(wrapper.find('.remove-button').exists()).toBe(false);
+ });
+ });
+
+ describe('when there are multiple links', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: release.assets.links.slice(0, 2),
+ },
+ },
+ });
+ });
+
+ it('remove asset link button should be visible', () => {
+ expect(wrapper.find('.remove-button').exists()).toBe(true);
+ });
+ });
+ });
+
describe('empty state', () => {
describe('when the release fetched from the API has no links', () => {
beforeEach(() => {
@@ -325,12 +361,6 @@ describe('Release edit component', () => {
it('does not call the addEmptyAssetLink store method when the component is created', () => {
expect(actions.addEmptyAssetLink).not.toHaveBeenCalled();
});
-
- it('calls addEmptyAssetLink when the final link is deleted by the user', () => {
- wrapper.find('.remove-button').vm.$emit('click');
-
- expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1);
- });
});
});
});
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
deleted file mode 100644
index 962ff068b92..00000000000
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ /dev/null
@@ -1,151 +0,0 @@
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
-import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
-import { getStoreConfig } from '~/reports/codequality_report/store';
-import { STATUS_NOT_FOUND } from '~/reports/constants';
-import { parsedReportIssues } from './mock_data';
-
-Vue.use(Vuex);
-
-describe('Grouped code quality reports app', () => {
- let wrapper;
- let mockStore;
-
- const PATHS = {
- codequalityHelpPath: 'codequality_help.html',
- baseBlobPath: 'base/blob/path/',
- headBlobPath: 'head/blob/path/',
- };
-
- const mountComponent = (props = {}) => {
- wrapper = mount(GroupedCodequalityReportsApp, {
- store: mockStore,
- propsData: {
- ...PATHS,
- ...props,
- },
- });
- };
-
- const findWidget = () => wrapper.find('.js-codequality-widget');
- const findIssueBody = () => wrapper.findComponent(CodequalityIssueBody);
-
- beforeEach(() => {
- const { state, ...storeConfig } = getStoreConfig();
- mockStore = new Vuex.Store({
- ...storeConfig,
- actions: {
- setPaths: () => {},
- fetchReports: () => {},
- },
- state: {
- ...state,
- ...PATHS,
- },
- });
-
- mountComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when it is loading reports', () => {
- beforeEach(() => {
- mockStore.state.isLoading = true;
- });
-
- it('should render loading text', () => {
- expect(findWidget().text()).toEqual('Loading Code quality report');
- });
- });
-
- describe('when base and head reports are loaded and compared', () => {
- describe('with no issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = [];
- mockStore.state.resolvedIssues = [];
- });
-
- it('renders no changes text', () => {
- expect(findWidget().text()).toEqual('No changes to code quality');
- });
- });
-
- describe('with issues', () => {
- describe('with new issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = parsedReportIssues.newIssues;
- mockStore.state.resolvedIssues = [];
- });
-
- it('renders summary text', () => {
- expect(findWidget().text()).toContain('Code quality degraded');
- });
-
- it('renders custom codequality issue body', () => {
- expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.newIssues[0]);
- });
- });
-
- describe('with resolved issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = [];
- mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
- });
-
- it('renders summary text', () => {
- expect(findWidget().text()).toContain('Code quality improved');
- });
-
- it('renders custom codequality issue body', () => {
- expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.resolvedIssues[0]);
- });
- });
-
- describe('with new and resolved issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = parsedReportIssues.newIssues;
- mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
- });
-
- it('renders summary text', () => {
- expect(findWidget().text()).toContain(
- 'Code quality scanning detected 2 changes in merged results',
- );
- });
-
- it('renders custom codequality issue body', () => {
- expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.newIssues[0]);
- });
- });
- });
- });
-
- describe('on error', () => {
- beforeEach(() => {
- mockStore.state.hasError = true;
- });
-
- it('renders error text', () => {
- expect(findWidget().text()).toContain('Failed to load Code quality report');
- });
-
- it('does not render a help icon', () => {
- expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(false);
- });
-
- describe('when base report was not found', () => {
- beforeEach(() => {
- mockStore.state.status = STATUS_NOT_FOUND;
- });
-
- it('renders a help icon with more information', () => {
- expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
index 71f1a0f4de0..1878b9f44b2 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -28,7 +28,6 @@ describe('Codequality Reports actions', () => {
baseBlobPath: 'baseBlobPath',
headBlobPath: 'headBlobPath',
reportsPath: 'reportsPath',
- helpPath: 'codequalityHelpPath',
};
return testAction(
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
index 111757e2d30..311a67a3e31 100644
--- a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
@@ -13,7 +13,7 @@ Object {
exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
Object {
- "component": "TestIssueBody",
+ "component": "CodequalityIssueBody",
"iconComponent": "IssueStatusIcon",
"isNew": false,
"issue": Object {
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js
index 6c0275dc47d..cacbde590d6 100644
--- a/spec/frontend/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/reports/components/grouped_issues_list_spec.js
@@ -74,7 +74,7 @@ describe('Grouped Issues List', () => {
createComponent({
propsData: {
resolvedIssues: [{ name: 'foo' }],
- component: 'TestIssueBody',
+ component: 'CodequalityIssueBody',
},
stubs: {
ReportItem,
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js
index b52c163eb26..60c7e5f2b44 100644
--- a/spec/frontend/reports/components/report_item_spec.js
+++ b/spec/frontend/reports/components/report_item_spec.js
@@ -10,7 +10,7 @@ describe('ReportItem', () => {
const wrapper = shallowMount(ReportItem, {
propsData: {
issue: { foo: 'bar' },
- component: componentNames.TestIssueBody,
+ component: componentNames.CodequalityIssueBody,
status: STATUS_SUCCESS,
showReportSectionStatusIcon: false,
},
@@ -23,7 +23,7 @@ describe('ReportItem', () => {
const wrapper = shallowMount(ReportItem, {
propsData: {
issue: { foo: 'bar' },
- component: componentNames.TestIssueBody,
+ component: componentNames.CodequalityIssueBody,
status: STATUS_SUCCESS,
},
});
diff --git a/spec/frontend/reports/grouped_test_report/components/modal_spec.js b/spec/frontend/reports/grouped_test_report/components/modal_spec.js
deleted file mode 100644
index e8564d2428d..00000000000
--- a/spec/frontend/reports/grouped_test_report/components/modal_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-
-import ReportsModal from '~/reports/grouped_test_report/components/modal.vue';
-import state from '~/reports/grouped_test_report/store/state';
-import CodeBlock from '~/vue_shared/components/code_block.vue';
-
-const StubbedGlModal = { template: '<div><slot></slot></div>', name: 'GlModal', props: ['title'] };
-
-describe('Grouped Test Reports Modal', () => {
- const modalDataStructure = state().modal.data;
- const title = 'Test#sum when a is 1 and b is 2 returns summary';
-
- // populate data
- modalDataStructure.execution_time.value = 0.009411;
- modalDataStructure.system_output.value = 'Failure/Error: is_expected.to eq(3)\n\n';
- modalDataStructure.filename.value = {
- text: 'link',
- path: '/file/path',
- };
-
- let wrapper;
-
- beforeEach(() => {
- wrapper = extendedWrapper(
- shallowMount(ReportsModal, {
- propsData: {
- title,
- modalData: modalDataStructure,
- visible: true,
- },
- stubs: { GlModal: StubbedGlModal, GlSprintf },
- }),
- );
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders code block', () => {
- expect(wrapper.findComponent(CodeBlock).props().code).toEqual(
- modalDataStructure.system_output.value,
- );
- });
-
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
-
- expect(link.attributes().href).toEqual(modalDataStructure.filename.value.path);
-
- expect(link.text()).toEqual(modalDataStructure.filename.value.text);
- });
-
- it('renders seconds', () => {
- expect(wrapper.text()).toContain(`${modalDataStructure.execution_time.value} s`);
- });
-
- it('render title', () => {
- expect(wrapper.findComponent(StubbedGlModal).props().title).toEqual(title);
- });
-
- it('re-emits hide event', () => {
- wrapper.findComponent(StubbedGlModal).vm.$emit('hide');
- expect(wrapper.emitted().hide).toEqual([[]]);
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js b/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
deleted file mode 100644
index 8a854a92ad7..00000000000
--- a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { GlBadge, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
-import TestIssueBody from '~/reports/grouped_test_report/components/test_issue_body.vue';
-import { failedIssue, successIssue } from '../../mock_data/mock_data';
-
-Vue.use(Vuex);
-
-describe('Test issue body', () => {
- let wrapper;
- let store;
-
- const findDescription = () => wrapper.findByTestId('test-issue-body-description');
- const findStatusIcon = () => wrapper.findComponent(IssueStatusIcon);
- const findBadge = () => wrapper.findComponent(GlBadge);
-
- const actionSpies = {
- openModal: jest.fn(),
- };
-
- const createComponent = ({ issue = failedIssue } = {}) => {
- store = new Vuex.Store({
- actions: actionSpies,
- });
-
- wrapper = extendedWrapper(
- shallowMount(TestIssueBody, {
- store,
- propsData: {
- issue,
- },
- stubs: {
- GlBadge,
- GlButton,
- IssueStatusIcon,
- },
- }),
- );
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when issue has failed status', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders issue name', () => {
- expect(findDescription().text()).toContain(failedIssue.name);
- });
-
- it('renders failed status icon', () => {
- expect(findStatusIcon().props('status')).toBe('failed');
- });
-
- describe('when issue has recent failures', () => {
- it('renders recent failures badge', () => {
- expect(findBadge().exists()).toBe(true);
- });
- });
- });
-
- describe('when issue has success status', () => {
- beforeEach(() => {
- createComponent({ issue: successIssue });
- });
-
- it('does not render recent failures', () => {
- expect(findBadge().exists()).toBe(false);
- });
-
- it('renders issue name', () => {
- expect(findDescription().text()).toBe(successIssue.name);
- });
-
- it('renders success status icon', () => {
- expect(findStatusIcon().props('status')).toBe('success');
- });
- });
-
- describe('when clicking on an issue', () => {
- it('calls openModal action', () => {
- createComponent();
- wrapper.findComponent(GlButton).trigger('click');
-
- expect(actionSpies.openModal).toHaveBeenCalledWith(expect.any(Object), {
- issue: failedIssue,
- });
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
deleted file mode 100644
index 90edb27d1d6..00000000000
--- a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
+++ /dev/null
@@ -1,355 +0,0 @@
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import Api from '~/api';
-import GroupedTestReportsApp from '~/reports/grouped_test_report/grouped_test_reports_app.vue';
-import { getStoreConfig } from '~/reports/grouped_test_report/store';
-
-import { failedReport } from '../mock_data/mock_data';
-import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
-import newErrorsTestReports from '../mock_data/new_errors_report.json';
-import newFailedTestReports from '../mock_data/new_failures_report.json';
-import successTestReports from '../mock_data/no_failures_report.json';
-import recentFailuresTestReports from '../mock_data/recent_failures_report.json';
-import resolvedFailures from '../mock_data/resolved_failures.json';
-
-jest.mock('~/api.js');
-
-Vue.use(Vuex);
-
-describe('Grouped test reports app', () => {
- const endpoint = 'endpoint.json';
- const headBlobPath = '/blob/path';
- const pipelinePath = '/path/to/pipeline';
- let wrapper;
- let mockStore;
-
- const mountComponent = ({ props = { pipelinePath } } = {}) => {
- wrapper = mount(GroupedTestReportsApp, {
- store: mockStore,
- propsData: {
- endpoint,
- headBlobPath,
- pipelinePath,
- ...props,
- },
- });
- };
-
- const setReports = (reports) => {
- mockStore.state.status = reports.status;
- mockStore.state.summary = reports.summary;
- mockStore.state.reports = reports.suites;
- };
-
- const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]');
- const findExpandButton = () => wrapper.find('[data-testid="report-section-expand-button"]');
- const findFullTestReportLink = () => wrapper.find('[data-testid="group-test-reports-full-link"]');
- const findSummaryDescription = () => wrapper.find('[data-testid="summary-row-description"]');
- const findIssueListUnresolvedHeading = () => wrapper.find('[data-testid="unresolvedHeading"]');
- const findIssueListResolvedHeading = () => wrapper.find('[data-testid="resolvedHeading"]');
- const findIssueDescription = () => wrapper.find('[data-testid="test-issue-body-description"]');
- const findIssueRecentFailures = () =>
- wrapper.find('[data-testid="test-issue-body-recent-failures"]');
- const findAllIssueDescriptions = () =>
- wrapper.findAll('[data-testid="test-issue-body-description"]');
-
- beforeEach(() => {
- mockStore = new Vuex.Store({
- ...getStoreConfig(),
- actions: {
- fetchReports: () => {},
- setPaths: () => {},
- },
- });
- mountComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('with success result', () => {
- beforeEach(() => {
- setReports(successTestReports);
- mountComponent();
- });
-
- it('renders success summary text', () => {
- expect(findHeader().text()).toBe(
- 'Test summary contained no changed test results out of 11 total tests',
- );
- });
- });
-
- describe('`View full report` button', () => {
- it('should render the full test report link', () => {
- const fullTestReportLink = findFullTestReportLink();
-
- expect(fullTestReportLink.exists()).toBe(true);
- expect(pipelinePath).not.toBe('');
- expect(fullTestReportLink.attributes('href')).toBe(`${pipelinePath}/test_report`);
- });
-
- describe('Without a pipelinePath', () => {
- beforeEach(() => {
- mountComponent({
- props: { pipelinePath: '' },
- });
- });
-
- it('should not render the full test report link', () => {
- expect(findFullTestReportLink().exists()).toBe(false);
- });
- });
- });
-
- describe('`Expand` button', () => {
- beforeEach(() => {
- setReports(newFailedTestReports);
- });
-
- it('tracks service ping metric', () => {
- mountComponent();
- findExpandButton().trigger('click');
-
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(wrapper.vm.$options.expandEvent);
- });
-
- it('only tracks the first expansion', () => {
- mountComponent();
- const expandButton = findExpandButton();
- expandButton.trigger('click');
- expandButton.trigger('click');
- expandButton.trigger('click');
-
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('with new failed result', () => {
- beforeEach(() => {
- setReports(newFailedTestReports);
- mountComponent();
- });
-
- it('renders New heading', () => {
- expect(findIssueListUnresolvedHeading().text()).toBe('New');
- });
-
- it('renders failed summary text', () => {
- expect(findHeader().text()).toBe('Test summary contained 2 failed out of 11 total tests');
- });
-
- it('renders failed test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'rspec:pg found 2 failed out of 8 total tests',
- );
- });
-
- it('renders failed issue in list', () => {
- expect(findIssueDescription().text()).toContain(
- 'Test#sum when a is 1 and b is 2 returns summary',
- );
- });
- });
-
- describe('with new error result', () => {
- beforeEach(() => {
- setReports(newErrorsTestReports);
- mountComponent();
- });
-
- it('renders New heading', () => {
- expect(findIssueListUnresolvedHeading().text()).toBe('New');
- });
-
- it('renders error summary text', () => {
- expect(findHeader().text()).toBe('Test summary contained 2 errors out of 11 total tests');
- });
-
- it('renders error test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'karma found 2 errors out of 3 total tests',
- );
- });
-
- it('renders error issue in list', () => {
- expect(findIssueDescription().text()).toContain(
- 'Test#sum when a is 1 and b is 2 returns summary',
- );
- });
- });
-
- describe('with mixed results', () => {
- beforeEach(() => {
- setReports(mixedResultsTestReports);
- mountComponent();
- });
-
- it('renders New and Fixed headings', () => {
- expect(findIssueListUnresolvedHeading().text()).toBe('New');
- expect(findIssueListResolvedHeading().text()).toBe('Fixed');
- });
-
- it('renders summary text', () => {
- expect(findHeader().text()).toBe(
- 'Test summary contained 2 failed and 2 fixed test results out of 11 total tests',
- );
- });
-
- it('renders failed test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'rspec:pg found 1 failed and 2 fixed test results out of 8 total tests',
- );
- });
-
- it('renders failed issue in list', () => {
- expect(findIssueDescription().text()).toContain(
- 'Test#subtract when a is 2 and b is 1 returns correct result',
- );
- });
- });
-
- describe('with resolved failures and resolved errors', () => {
- beforeEach(() => {
- setReports(resolvedFailures);
- mountComponent();
- });
-
- it('renders Fixed heading', () => {
- expect(findIssueListResolvedHeading().text()).toBe('Fixed');
- });
-
- it('renders summary text', () => {
- expect(findHeader().text()).toBe(
- 'Test summary contained 4 fixed test results out of 11 total tests',
- );
- });
-
- it('renders resolved test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'rspec:pg found 4 fixed test results out of 8 total tests',
- );
- });
-
- it('renders resolved failures', () => {
- expect(findIssueDescription().text()).toContain(
- resolvedFailures.suites[0].resolved_failures[0].name,
- );
- });
-
- it('renders resolved errors', () => {
- expect(findAllIssueDescriptions().at(2).text()).toContain(
- resolvedFailures.suites[0].resolved_errors[0].name,
- );
- });
- });
-
- describe('recent failures counts', () => {
- describe('with recent failures counts', () => {
- beforeEach(() => {
- setReports(recentFailuresTestReports);
- mountComponent();
- });
-
- it('renders the recently failed tests summary', () => {
- expect(findHeader().text()).toContain(
- '2 out of 3 failed tests have failed more than once in the last 14 days',
- );
- });
-
- it('renders the recently failed count on the test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- '1 out of 2 failed tests has failed more than once in the last 14 days',
- );
- });
-
- it('renders the recent failures count on the test case', () => {
- expect(findIssueRecentFailures().text()).toBe('Failed 8 times in main in the last 14 days');
- });
- });
-
- describe('without recent failures counts', () => {
- beforeEach(() => {
- setReports(mixedResultsTestReports);
- mountComponent();
- });
-
- it('does not render the recently failed tests summary', () => {
- expect(findHeader().text()).not.toContain('failed more than once in the last 14 days');
- });
-
- it('does not render the recently failed count on the test suite', () => {
- expect(findSummaryDescription().text()).not.toContain(
- 'failed more than once in the last 14 days',
- );
- });
-
- it('does not render the recent failures count on the test case', () => {
- expect(findIssueDescription().text()).not.toContain('in the last 14 days');
- });
- });
- });
-
- describe('with a report that failed to load', () => {
- beforeEach(() => {
- setReports(failedReport);
- mountComponent();
- });
-
- it('renders an error status for the report', () => {
- const { name } = failedReport.suites[0];
-
- expect(findSummaryDescription().text()).toContain(
- `An error occurred while loading ${name} result`,
- );
- });
- });
-
- describe('with a report parsing errors', () => {
- beforeEach(() => {
- const reports = failedReport;
- reports.suites[0].suite_errors = {
- head: 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
- base: 'JUnit data parsing failed: string not matched',
- };
- setReports(reports);
- mountComponent();
- });
-
- it('renders the error messages', () => {
- expect(findSummaryDescription().text()).toContain(
- 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
- );
- expect(findSummaryDescription().text()).toContain(
- 'JUnit data parsing failed: string not matched',
- );
- });
- });
-
- describe('with error', () => {
- beforeEach(() => {
- mockStore.state.isLoading = false;
- mockStore.state.hasError = true;
- mountComponent();
- });
-
- it('renders loading state', () => {
- expect(findHeader().text()).toBe('Test summary failed loading results');
- });
- });
-
- describe('while loading', () => {
- beforeEach(() => {
- mockStore.state.isLoading = true;
- mountComponent();
- });
-
- it('renders loading state', () => {
- expect(findHeader().text()).toBe('Test summary results are being parsed');
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/actions_spec.js b/spec/frontend/reports/grouped_test_report/store/actions_spec.js
deleted file mode 100644
index 7469c31cf84..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/actions_spec.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
-import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import {
- setPaths,
- requestReports,
- fetchReports,
- stopPolling,
- clearEtagPoll,
- receiveReportsSuccess,
- receiveReportsError,
- openModal,
- closeModal,
-} from '~/reports/grouped_test_report/store/actions';
-import * as types from '~/reports/grouped_test_report/store/mutation_types';
-import state from '~/reports/grouped_test_report/store/state';
-
-describe('Reports Store Actions', () => {
- let mockedState;
-
- beforeEach(() => {
- mockedState = state();
- });
-
- describe('setPaths', () => {
- it('should commit SET_PATHS mutation', () => {
- return testAction(
- setPaths,
- { endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
- mockedState,
- [
- {
- type: types.SET_PATHS,
- payload: { endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
- },
- ],
- [],
- );
- });
- });
-
- describe('requestReports', () => {
- it('should commit REQUEST_REPORTS mutation', () => {
- return testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], []);
- });
- });
-
- describe('fetchReports', () => {
- let mock;
-
- beforeEach(() => {
- mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- stopPolling();
- clearEtagPoll();
- });
-
- describe('success', () => {
- it('dispatches requestReports and receiveReportsSuccess', () => {
- mock
- .onGet(`${TEST_HOST}/endpoint.json`)
- .replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
-
- return testAction(
- fetchReports,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestReports',
- },
- {
- payload: { data: { summary: {}, suites: [{ name: 'rspec' }] }, status: 200 },
- type: 'receiveReportsSuccess',
- },
- ],
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- });
-
- it('dispatches requestReports and receiveReportsError', () => {
- return testAction(
- fetchReports,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestReports',
- },
- {
- type: 'receiveReportsError',
- },
- ],
- );
- });
- });
- });
-
- describe('receiveReportsSuccess', () => {
- it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', () => {
- return testAction(
- receiveReportsSuccess,
- { data: { summary: {} }, status: 200 },
- mockedState,
- [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
- [],
- );
- });
-
- it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', () => {
- return testAction(
- receiveReportsSuccess,
- { data: { summary: {} }, status: 204 },
- mockedState,
- [],
- [],
- );
- });
- });
-
- describe('receiveReportsError', () => {
- it('should commit RECEIVE_REPORTS_ERROR mutation', () => {
- return testAction(
- receiveReportsError,
- null,
- mockedState,
- [{ type: types.RECEIVE_REPORTS_ERROR }],
- [],
- );
- });
- });
-
- describe('openModal', () => {
- it('should commit SET_ISSUE_MODAL_DATA', () => {
- return testAction(
- openModal,
- { name: 'foo' },
- mockedState,
- [{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }],
- [],
- );
- });
- });
-
- describe('closeModal', () => {
- it('should commit RESET_ISSUE_MODAL_DATA', () => {
- return testAction(
- closeModal,
- {},
- mockedState,
- [{ type: types.RESET_ISSUE_MODAL_DATA, payload: {} }],
- [],
- );
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/mutations_spec.js b/spec/frontend/reports/grouped_test_report/store/mutations_spec.js
deleted file mode 100644
index b2890d7285f..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/mutations_spec.js
+++ /dev/null
@@ -1,162 +0,0 @@
-import * as types from '~/reports/grouped_test_report/store/mutation_types';
-import mutations from '~/reports/grouped_test_report/store/mutations';
-import state from '~/reports/grouped_test_report/store/state';
-import { failedIssue } from '../../mock_data/mock_data';
-
-describe('Reports Store Mutations', () => {
- let stateCopy;
-
- beforeEach(() => {
- stateCopy = state();
- });
-
- describe('SET_PATHS', () => {
- it('should set endpoint', () => {
- mutations[types.SET_PATHS](stateCopy, {
- endpoint: 'endpoint.json',
- headBlobPath: '/blob/path',
- });
-
- expect(stateCopy.endpoint).toEqual('endpoint.json');
- expect(stateCopy.headBlobPath).toEqual('/blob/path');
- });
- });
-
- describe('REQUEST_REPORTS', () => {
- it('should set isLoading to true', () => {
- mutations[types.REQUEST_REPORTS](stateCopy);
-
- expect(stateCopy.isLoading).toEqual(true);
- });
- });
-
- describe('RECEIVE_REPORTS_SUCCESS', () => {
- const mockedResponse = {
- summary: {
- total: 14,
- resolved: 0,
- failed: 7,
- },
- suites: [
- {
- name: 'build:linux',
- summary: {
- total: 2,
- resolved: 0,
- failed: 1,
- },
- new_failures: [
- {
- name: 'StringHelper#concatenate when a is git and b is lab returns summary',
- execution_time: 0.0092435,
- system_output: "Failure/Error: is_expected.to eq('gitlab')",
- recent_failures: {
- count: 4,
- base_branch: 'main',
- },
- },
- ],
- resolved_failures: [
- {
- name: 'StringHelper#concatenate when a is git and b is lab returns summary',
- execution_time: 0.009235,
- system_output: "Failure/Error: is_expected.to eq('gitlab')",
- },
- ],
- existing_failures: [
- {
- name: 'StringHelper#concatenate when a is git and b is lab returns summary',
- execution_time: 1232.08,
- system_output: "Failure/Error: is_expected.to eq('gitlab')",
- },
- ],
- },
- ],
- };
-
- beforeEach(() => {
- mutations[types.RECEIVE_REPORTS_SUCCESS](stateCopy, mockedResponse);
- });
-
- it('should reset isLoading', () => {
- expect(stateCopy.isLoading).toEqual(false);
- });
-
- it('should reset hasError', () => {
- expect(stateCopy.hasError).toEqual(false);
- });
-
- it('should set summary counts', () => {
- expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total);
- expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved);
- expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed);
- expect(stateCopy.summary.recentlyFailed).toEqual(1);
- });
-
- it('should set reports', () => {
- expect(stateCopy.reports).toEqual(mockedResponse.suites);
- });
- });
-
- describe('RECEIVE_REPORTS_ERROR', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
- });
-
- it('should reset isLoading', () => {
- expect(stateCopy.isLoading).toEqual(false);
- });
-
- it('should set hasError to true', () => {
- expect(stateCopy.hasError).toEqual(true);
- });
-
- it('should reset reports', () => {
- expect(stateCopy.reports).toEqual([]);
- });
- });
-
- describe('SET_ISSUE_MODAL_DATA', () => {
- beforeEach(() => {
- mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
- issue: failedIssue,
- });
- });
-
- it('should set modal title', () => {
- expect(stateCopy.modal.title).toEqual(failedIssue.name);
- });
-
- it('should set modal data', () => {
- expect(stateCopy.modal.data.execution_time.value).toEqual(failedIssue.execution_time);
- expect(stateCopy.modal.data.system_output.value).toEqual(failedIssue.system_output);
- });
-
- it('should open modal', () => {
- expect(stateCopy.modal.open).toEqual(true);
- });
- });
-
- describe('RESET_ISSUE_MODAL_DATA', () => {
- beforeEach(() => {
- mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
- issue: failedIssue,
- });
-
- mutations[types.RESET_ISSUE_MODAL_DATA](stateCopy);
- });
-
- it('should reset modal title', () => {
- expect(stateCopy.modal.title).toEqual(null);
- });
-
- it('should reset modal data', () => {
- expect(stateCopy.modal.data.execution_time.value).toEqual(null);
- expect(stateCopy.modal.data.system_output.value).toEqual(null);
- });
-
- it('should close modal', () => {
- expect(stateCopy.modal.open).toEqual(false);
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/utils_spec.js b/spec/frontend/reports/grouped_test_report/store/utils_spec.js
deleted file mode 100644
index 760afe1c11a..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/utils_spec.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import {
- STATUS_FAILED,
- STATUS_SUCCESS,
- ICON_WARNING,
- ICON_SUCCESS,
- ICON_NOTFOUND,
-} from '~/reports/constants';
-import * as utils from '~/reports/grouped_test_report/store/utils';
-
-describe('Reports store utils', () => {
- describe('summaryTextbuilder', () => {
- it('should render text for no changed results in multiple tests', () => {
- const name = 'Test summary';
- const data = { total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained no changed test results out of 10 total tests');
- });
-
- it('should render text for no changed results in one test', () => {
- const name = 'Test summary';
- const data = { total: 1 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained no changed test results out of 1 total test');
- });
-
- it('should render text for multiple failed results', () => {
- const name = 'Test summary';
- const data = { failed: 3, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained 3 failed out of 10 total tests');
- });
-
- it('should render text for multiple errored results', () => {
- const name = 'Test summary';
- const data = { errored: 7, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained 7 errors out of 10 total tests');
- });
-
- it('should render text for multiple fixed results', () => {
- const name = 'Test summary';
- const data = { resolved: 4, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained 4 fixed test results out of 10 total tests');
- });
-
- it('should render text for multiple fixed, and multiple failed results', () => {
- const name = 'Test summary';
- const data = { failed: 3, resolved: 4, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 3 failed and 4 fixed test results out of 10 total tests',
- );
- });
-
- it('should render text for a singular fixed, and a singular failed result', () => {
- const name = 'Test summary';
- const data = { failed: 1, resolved: 1, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 1 failed and 1 fixed test result out of 10 total tests',
- );
- });
-
- it('should render text for singular failed, errored, and fixed results', () => {
- const name = 'Test summary';
- const data = { failed: 1, errored: 1, resolved: 1, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 1 failed, 1 error and 1 fixed test result out of 10 total tests',
- );
- });
-
- it('should render text for multiple failed, errored, and fixed results', () => {
- const name = 'Test summary';
- const data = { failed: 2, errored: 3, resolved: 4, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 2 failed, 3 errors and 4 fixed test results out of 10 total tests',
- );
- });
- });
-
- describe('reportTextBuilder', () => {
- it('should render text for no changed results in multiple tests', () => {
- const name = 'Rspec';
- const data = { total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found no changed test results out of 10 total tests');
- });
-
- it('should render text for no changed results in one test', () => {
- const name = 'Rspec';
- const data = { total: 1 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found no changed test results out of 1 total test');
- });
-
- it('should render text for multiple failed results', () => {
- const name = 'Rspec';
- const data = { failed: 3, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 3 failed out of 10 total tests');
- });
-
- it('should render text for multiple errored results', () => {
- const name = 'Rspec';
- const data = { errored: 7, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 7 errors out of 10 total tests');
- });
-
- it('should render text for multiple fixed results', () => {
- const name = 'Rspec';
- const data = { resolved: 4, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 4 fixed test results out of 10 total tests');
- });
-
- it('should render text for multiple fixed, and multiple failed results', () => {
- const name = 'Rspec';
- const data = { failed: 3, resolved: 4, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 3 failed and 4 fixed test results out of 10 total tests');
- });
-
- it('should render text for a singular fixed, and a singular failed result', () => {
- const name = 'Rspec';
- const data = { failed: 1, resolved: 1, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 1 failed and 1 fixed test result out of 10 total tests');
- });
-
- it('should render text for singular failed, errored, and fixed results', () => {
- const name = 'Rspec';
- const data = { failed: 1, errored: 1, resolved: 1, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe(
- 'Rspec found 1 failed, 1 error and 1 fixed test result out of 10 total tests',
- );
- });
-
- it('should render text for multiple failed, errored, and fixed results', () => {
- const name = 'Rspec';
- const data = { failed: 2, errored: 3, resolved: 4, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe(
- 'Rspec found 2 failed, 3 errors and 4 fixed test results out of 10 total tests',
- );
- });
- });
-
- describe('recentFailuresTextBuilder', () => {
- it.each`
- recentlyFailed | failed | expected
- ${0} | ${1} | ${''}
- ${1} | ${1} | ${'1 out of 1 failed test has failed more than once in the last 14 days'}
- ${1} | ${2} | ${'1 out of 2 failed tests has failed more than once in the last 14 days'}
- ${2} | ${3} | ${'2 out of 3 failed tests have failed more than once in the last 14 days'}
- `(
- 'should render summary for $recentlyFailed out of $failed failures',
- ({ recentlyFailed, failed, expected }) => {
- const result = utils.recentFailuresTextBuilder({ recentlyFailed, failed });
-
- expect(result).toBe(expected);
- },
- );
- });
-
- describe('countRecentlyFailedTests', () => {
- it('counts tests with more than one recent failure in a report', () => {
- const report = {
- new_failures: [{ recent_failures: { count: 2 } }],
- existing_failures: [{ recent_failures: { count: 1 } }],
- resolved_failures: [{ recent_failures: { count: 20 } }, { recent_failures: { count: 5 } }],
- };
- const result = utils.countRecentlyFailedTests(report);
-
- expect(result).toBe(3);
- });
-
- it('counts tests with more than one recent failure in an array of reports', () => {
- const reports = [
- {
- new_failures: [{ recent_failures: { count: 2 } }],
- existing_failures: [
- { recent_failures: { count: 20 } },
- { recent_failures: { count: 5 } },
- ],
- resolved_failures: [{ recent_failures: { count: 2 } }],
- },
- {
- new_failures: [{ recent_failures: { count: 8 } }, { recent_failures: { count: 14 } }],
- existing_failures: [{ recent_failures: { count: 1 } }],
- resolved_failures: [{ recent_failures: { count: 7 } }, { recent_failures: { count: 5 } }],
- },
- ];
- const result = utils.countRecentlyFailedTests(reports);
-
- expect(result).toBe(8);
- });
- });
-
- describe('statusIcon', () => {
- describe('with failed status', () => {
- it('returns ICON_WARNING', () => {
- expect(utils.statusIcon(STATUS_FAILED)).toEqual(ICON_WARNING);
- });
- });
-
- describe('with success status', () => {
- it('returns ICON_SUCCESS', () => {
- expect(utils.statusIcon(STATUS_SUCCESS)).toEqual(ICON_SUCCESS);
- });
- });
-
- describe('without a status', () => {
- it('returns ICON_NOTFOUND', () => {
- expect(utils.statusIcon()).toEqual(ICON_NOTFOUND);
- });
- });
- });
-
- describe('formatFilePath', () => {
- it.each`
- file | expected
- ${'./test.js'} | ${'test.js'}
- ${'/test.js'} | ${'test.js'}
- ${'.//////////////test.js'} | ${'test.js'}
- ${'test.js'} | ${'test.js'}
- ${'mock/path./test.js'} | ${'mock/path./test.js'}
- ${'./mock/path./test.js'} | ${'mock/path./test.js'}
- `('should format $file to be $expected', ({ file, expected }) => {
- expect(utils.formatFilePath(file)).toBe(expected);
- });
- });
-});
diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
deleted file mode 100644
index d1f04f0ee37..00000000000
--- a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { nextTick } from 'vue';
-import { GlBanner } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-
-describe('RunnerStackedLayoutBanner', () => {
- let wrapper;
-
- const findBanner = () => wrapper.findComponent(GlBanner);
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
-
- const createComponent = ({ ...options } = {}, mountFn = shallowMount) => {
- wrapper = mountFn(RunnerStackedLayoutBanner, {
- ...options,
- });
- };
-
- it('Displays a banner', () => {
- createComponent();
-
- expect(findBanner().props()).toMatchObject({
- svgPath: expect.stringContaining('data:image/svg+xml;utf8,'),
- title: expect.any(String),
- buttonText: expect.any(String),
- buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'),
- });
- expect(findLocalStorageSync().exists()).toBe(true);
- });
-
- it('Does not display a banner when dismissed', async () => {
- createComponent();
-
- findLocalStorageSync().vm.$emit('input', true);
-
- await nextTick();
-
- expect(findBanner().exists()).toBe(false);
- expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal
- });
-});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index 0542e96c77c..fa5ccfeb478 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -107,3 +107,87 @@ export const PROMISE_ALL_EXPECTED_MUTATIONS = {
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] },
},
};
+
+export const MOCK_NAVIGATION = {
+ projects: {
+ label: 'Projects',
+ scope: 'projects',
+ link: '/search?scope=projects&search=et',
+ count_link: '/search/count?scope=projects&search=et',
+ },
+ blobs: {
+ label: 'Code',
+ scope: 'blobs',
+ link: '/search?scope=blobs&search=et',
+ count_link: '/search/count?scope=blobs&search=et',
+ },
+ issues: {
+ label: 'Issues',
+ scope: 'issues',
+ link: '/search?scope=issues&search=et',
+ active: true,
+ count: '2,430',
+ },
+ merge_requests: {
+ label: 'Merge requests',
+ scope: 'merge_requests',
+ link: '/search?scope=merge_requests&search=et',
+ count_link: '/search/count?scope=merge_requests&search=et',
+ },
+ wiki_blobs: {
+ label: 'Wiki',
+ scope: 'wiki_blobs',
+ link: '/search?scope=wiki_blobs&search=et',
+ count_link: '/search/count?scope=wiki_blobs&search=et',
+ },
+ commits: {
+ label: 'Commits',
+ scope: 'commits',
+ link: '/search?scope=commits&search=et',
+ count_link: '/search/count?scope=commits&search=et',
+ },
+ notes: {
+ label: 'Comments',
+ scope: 'notes',
+ link: '/search?scope=notes&search=et',
+ count_link: '/search/count?scope=notes&search=et',
+ },
+ milestones: {
+ label: 'Milestones',
+ scope: 'milestones',
+ link: '/search?scope=milestones&search=et',
+ count_link: '/search/count?scope=milestones&search=et',
+ },
+ users: {
+ label: 'Users',
+ scope: 'users',
+ link: '/search?scope=users&search=et',
+ count_link: '/search/count?scope=users&search=et',
+ },
+};
+
+export const MOCK_NAVIGATION_DATA = {
+ projects: {
+ label: 'Projects',
+ scope: 'projects',
+ link: '/search?scope=projects&search=et',
+ count_link: '/search/count?scope=projects&search=et',
+ },
+};
+
+export const MOCK_ENDPOINT_RESPONSE = { count: '13' };
+
+export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = {
+ projects: {
+ count: '13',
+ label: 'Projects',
+ scope: 'projects',
+ link: '/search?scope=projects&search=et',
+ count_link: '/search/count?scope=projects&search=et',
+ },
+};
+
+export const MOCK_NAVIGATION_ACTION_MUTATION = {
+ type: types.RECEIVE_NAVIGATION_COUNT,
+ payload: { key: 'projects', count: '13' },
+};
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 89959feec39..e87217950cd 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -1,11 +1,10 @@
-import { GlButton, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
-import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
-import StatusFilter from '~/search/sidebar/components/status_filter.vue';
+import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
+import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
Vue.use(Vuex);
@@ -17,7 +16,7 @@ describe('GlobalSearchSidebar', () => {
resetQuery: jest.fn(),
};
- const createComponent = (initialState) => {
+ const createComponent = (initialState, featureFlags) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -28,6 +27,11 @@ describe('GlobalSearchSidebar', () => {
wrapper = shallowMount(GlobalSearchSidebar, {
store,
+ provide: {
+ glFeatures: {
+ ...featureFlags,
+ },
+ },
});
};
@@ -35,118 +39,68 @@ describe('GlobalSearchSidebar', () => {
wrapper.destroy();
});
- const findSidebarForm = () => wrapper.find('form');
- const findStatusFilter = () => wrapper.findComponent(StatusFilter);
- const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
- const findApplyButton = () => wrapper.findComponent(GlButton);
- const findResetLinkButton = () => wrapper.findComponent(GlLink);
+ const findSidebarSection = () => wrapper.find('section');
+ const findFilters = () => wrapper.findComponent(ResultsFilters);
+ const findSidebarNavigation = () => wrapper.findComponent(ScopeNavigation);
- describe('template', () => {
+ describe('renders properly', () => {
describe('scope=projects', () => {
beforeEach(() => {
createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'projects' } });
});
- it("doesn't render StatusFilter", () => {
- expect(findStatusFilter().exists()).toBe(false);
- });
-
- it("doesn't render ConfidentialityFilter", () => {
- expect(findConfidentialityFilter().exists()).toBe(false);
+ it('shows section', () => {
+ expect(findSidebarSection().exists()).toBe(true);
});
- it("doesn't render ApplyButton", () => {
- expect(findApplyButton().exists()).toBe(false);
+ it("doesn't shows filters", () => {
+ expect(findFilters().exists()).toBe(false);
});
});
- describe('scope=issues', () => {
+ describe('scope=merge_requests', () => {
beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY });
- });
- it('renders StatusFilter', () => {
- expect(findStatusFilter().exists()).toBe(true);
+ createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'merge_requests' } });
});
- it('renders ConfidentialityFilter', () => {
- expect(findConfidentialityFilter().exists()).toBe(true);
+ it('shows section', () => {
+ expect(findSidebarSection().exists()).toBe(true);
});
- it('renders ApplyButton', () => {
- expect(findApplyButton().exists()).toBe(true);
+ it('shows filters', () => {
+ expect(findFilters().exists()).toBe(true);
});
});
- });
- describe('ApplyButton', () => {
- describe('when sidebarDirty is false', () => {
+ describe('scope=issues', () => {
beforeEach(() => {
- createComponent({ sidebarDirty: false });
- });
-
- it('disables the button', () => {
- expect(findApplyButton().attributes('disabled')).toBe('true');
+ createComponent({ urlQuery: MOCK_QUERY });
});
- });
-
- describe('when sidebarDirty is true', () => {
- beforeEach(() => {
- createComponent({ sidebarDirty: true });
+ it('shows section', () => {
+ expect(findSidebarSection().exists()).toBe(true);
});
- it('enables the button', () => {
- expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ it('shows filters', () => {
+ expect(findFilters().exists()).toBe(true);
});
});
});
- describe('ResetLinkButton', () => {
- describe('with no filter selected', () => {
- beforeEach(() => {
- createComponent({ urlQuery: {} });
- });
-
- it('does not render', () => {
- expect(findResetLinkButton().exists()).toBe(false);
- });
- });
-
- describe('with filter selected', () => {
- beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY });
- });
-
- it('does render', () => {
- expect(findResetLinkButton().exists()).toBe(true);
- });
+ describe('when search_page_vertical_nav is enabled', () => {
+ beforeEach(() => {
+ createComponent({}, { searchPageVerticalNav: true });
});
-
- describe('with filter selected and user updated query back to default', () => {
- beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY, query: {} });
- });
-
- it('does render', () => {
- expect(findResetLinkButton().exists()).toBe(true);
- });
+ it('shows the vertical navigation', () => {
+ expect(findSidebarNavigation().exists()).toBe(true);
});
});
- describe('actions', () => {
+ describe('when search_page_vertical_nav is disabled', () => {
beforeEach(() => {
- createComponent({});
+ createComponent({}, { searchPageVerticalNav: false });
});
-
- it('clicking ApplyButton calls applyQuery', () => {
- findSidebarForm().trigger('submit');
-
- expect(actionSpies.applyQuery).toHaveBeenCalled();
- });
-
- it('clicking ResetLinkButton calls resetQuery', () => {
- findResetLinkButton().vm.$emit('click');
-
- expect(actionSpies.resetQuery).toHaveBeenCalled();
+ it('hides the vertical navigation', () => {
+ expect(findSidebarNavigation().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js
new file mode 100644
index 00000000000..4f217709297
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/filters_spec.js
@@ -0,0 +1,132 @@
+import { GlButton, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { MOCK_QUERY } from 'jest/search/mock_data';
+import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
+import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
+import StatusFilter from '~/search/sidebar/components/status_filter.vue';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchSidebarFilters', () => {
+ let wrapper;
+
+ const actionSpies = {
+ applyQuery: jest.fn(),
+ resetQuery: jest.fn(),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(ResultsFilters, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSidebarForm = () => wrapper.find('form');
+ const findStatusFilter = () => wrapper.findComponent(StatusFilter);
+ const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
+ const findApplyButton = () => wrapper.findComponent(GlButton);
+ const findResetLinkButton = () => wrapper.findComponent(GlLink);
+
+ describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY });
+ });
+ it('renders StatusFilter', () => {
+ expect(findStatusFilter().exists()).toBe(true);
+ });
+
+ it('renders ConfidentialityFilter', () => {
+ expect(findConfidentialityFilter().exists()).toBe(true);
+ });
+
+ it('renders ApplyButton', () => {
+ expect(findApplyButton().exists()).toBe(true);
+ });
+ });
+
+ describe('ApplyButton', () => {
+ describe('when sidebarDirty is false', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: false });
+ });
+
+ it('disables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when sidebarDirty is true', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: true });
+ });
+
+ it('enables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('ResetLinkButton', () => {
+ describe('with no filter selected', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: {} });
+ });
+
+ it('does not render', () => {
+ expect(findResetLinkButton().exists()).toBe(false);
+ });
+ });
+
+ describe('with filter selected', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY });
+ });
+
+ it('does render', () => {
+ expect(findResetLinkButton().exists()).toBe(true);
+ });
+ });
+
+ describe('with filter selected and user updated query back to default', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY, query: {} });
+ });
+
+ it('does render', () => {
+ expect(findResetLinkButton().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent({});
+ });
+
+ it('clicking ApplyButton calls applyQuery', () => {
+ findSidebarForm().trigger('submit');
+
+ expect(actionSpies.applyQuery).toHaveBeenCalled();
+ });
+
+ it('clicking ResetLinkButton calls resetQuery', () => {
+ findResetLinkButton().vm.$emit('click');
+
+ expect(actionSpies.resetQuery).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
new file mode 100644
index 00000000000..6262a52e01a
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
@@ -0,0 +1,80 @@
+import { GlNav, GlNavItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { MOCK_QUERY, MOCK_NAVIGATION } from 'jest/search/mock_data';
+import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
+
+Vue.use(Vuex);
+
+describe('ScopeNavigation', () => {
+ let wrapper;
+
+ const actionSpies = {
+ fetchSidebarCount: jest.fn(),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ navigation: MOCK_NAVIGATION,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(ScopeNavigation, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findNavElement = () => wrapper.find('nav');
+ const findGlNav = () => wrapper.findComponent(GlNav);
+ const findGlNavItems = () => wrapper.findAllComponents(GlNavItem);
+ const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active'));
+ const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1);
+
+ describe('scope navigation', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders section', () => {
+ expect(findNavElement().exists()).toBe(true);
+ });
+
+ it('renders nav component', () => {
+ expect(findGlNav().exists()).toBe(true);
+ });
+
+ it('renders all nav item components', () => {
+ expect(findGlNavItems()).toHaveLength(9);
+ });
+
+ it('nav items have proper links', () => {
+ const linkAtPosition = 3;
+ const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
+
+ expect(findGlNavItems().at(linkAtPosition).attributes('href')).toBe(link);
+ });
+ });
+
+ describe('scope navigation sets proper state', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets proper class to active item', () => {
+ expect(findGlNavItemActive()).toHaveLength(1);
+ });
+
+ it('active item', () => {
+ expect(findGlNavItemActiveCount().text()).toBe('2.4K');
+ });
+ });
+});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index c442ffa521d..3d19b27ff86 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { createAlert } from '~/flash';
+import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
@@ -23,6 +24,9 @@ import {
MOCK_FRESH_DATA_RES,
PRELOAD_EXPECTED_MUTATIONS,
PROMISE_ALL_EXPECTED_MUTATIONS,
+ MOCK_NAVIGATION_DATA,
+ MOCK_NAVIGATION_ACTION_MUTATION,
+ MOCK_ENDPOINT_RESPONSE,
} from '../mock_data';
jest.mock('~/flash');
@@ -31,6 +35,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
joinPaths: jest.fn().mockReturnValue(''),
visitUrl: jest.fn(),
}));
+jest.mock('~/lib/logger', () => ({
+ logError: jest.fn(),
+}));
describe('Global Search Store Actions', () => {
let mock;
@@ -260,4 +267,32 @@ describe('Global Search Store Actions', () => {
);
});
});
+
+ describe.each`
+ action | axiosMock | type | scope | expectedMutations | errorLogs
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${'issues'} | ${[]} | ${1}
+ `('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => {
+ describe(`on ${type}`, () => {
+ beforeEach(() => {
+ state.navigation = MOCK_NAVIGATION_DATA;
+ state.urlQuery = {
+ scope,
+ };
+
+ if (axiosMock.method) {
+ mock[axiosMock.method]().reply(axiosMock.code, MOCK_ENDPOINT_RESPONSE);
+ }
+ });
+
+ it(`should ${expectedMutations.length === 0 ? 'NOT ' : ''}dispatch ${
+ expectedMutations.length === 0 ? '' : 'the correct '
+ }mutations for ${scope}`, () => {
+ return testAction({ action, state, expectedMutations }).then(() => {
+ expect(logger.logError).toHaveBeenCalledTimes(errorLogs);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index 25f9b692955..a79ec8f70b0 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -1,13 +1,20 @@
import * as types from '~/search/store/mutation_types';
import mutations from '~/search/store/mutations';
import createState from '~/search/store/state';
-import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
+import {
+ MOCK_QUERY,
+ MOCK_GROUPS,
+ MOCK_PROJECTS,
+ MOCK_NAVIGATION_DATA,
+ MOCK_NAVIGATION_ACTION_MUTATION,
+ MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION,
+} from '../mock_data';
describe('Global Search Store Mutations', () => {
let state;
beforeEach(() => {
- state = createState({ query: MOCK_QUERY });
+ state = createState({ query: MOCK_QUERY, navigation: MOCK_NAVIGATION_DATA });
});
describe('REQUEST_GROUPS', () => {
@@ -90,4 +97,15 @@ describe('Global Search Store Mutations', () => {
expect(state.frequentItems[payload.key]).toStrictEqual(payload.data);
});
});
+
+ describe('RECEIVE_NAVIGATION_COUNT', () => {
+ it('sets frequentItems[key] to data', () => {
+ const { payload } = MOCK_NAVIGATION_ACTION_MUTATION;
+ mutations[types.RECEIVE_NAVIGATION_COUNT](state, payload);
+
+ expect(state.navigation[payload.key]).toStrictEqual(
+ MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION[payload.key],
+ );
+ });
+ });
});
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
index 11841106ed0..efe3f7e8dbf 100644
--- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
+++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`self monitor component When the self monitor project has not been created default state to match the default snapshot 1`] = `
+exports[`self-monitor component When the self-monitor project has not been created default state to match the default snapshot 1`] = `
<section
class="settings no-animate js-self-monitoring-settings"
>
@@ -11,7 +11,7 @@ exports[`self monitor component When the self monitor project has not been creat
class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only"
>
- Self monitoring
+ Self-monitoring
</h4>
@@ -30,7 +30,7 @@ exports[`self monitor component When the self monitor project has not been creat
class="js-section-sub-header"
>
- Activate or deactivate instance self monitoring.
+ Activate or deactivate instance self-monitoring.
<gl-link-stub
href="/help/administration/monitoring/gitlab_self_monitoring_project/index"
@@ -47,7 +47,7 @@ exports[`self monitor component When the self monitor project has not been creat
name="self-monitoring-form"
>
<p>
- Activate self monitoring to create a project to use to monitor the health of your instance.
+ Activate self-monitoring to create a project to use to monitor the health of your instance.
</p>
<gl-form-group-stub
@@ -55,7 +55,7 @@ exports[`self monitor component When the self monitor project has not been creat
optionaltext="(optional)"
>
<gl-toggle-stub
- label="Self monitoring"
+ label="Self-monitoring"
labelposition="top"
/>
</gl-form-group-stub>
@@ -69,15 +69,15 @@ exports[`self monitor component When the self monitor project has not been creat
dismisslabel="Close"
modalclass=""
modalid="delete-self-monitor-modal"
- ok-title="Delete self monitoring project"
+ ok-title="Delete self-monitoring project"
ok-variant="danger"
size="md"
- title="Deactivate self monitoring?"
+ title="Deactivate self-monitoring?"
titletag="h4"
>
<div>
- Deactivating self monitoring deletes the self monitoring project. Are you sure you want to deactivate self monitoring and delete the project?
+ Deactivating self-monitoring deletes the self-monitoring project. Are you sure you want to deactivate self-monitoring and delete the project?
</div>
</gl-modal-stub>
diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
index c690bbf1c57..35f2734dde3 100644
--- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js
+++ b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
@@ -4,11 +4,11 @@ import { TEST_HOST } from 'helpers/test_constants';
import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
import { createStore } from '~/self_monitor/store';
-describe('self monitor component', () => {
+describe('self-monitor component', () => {
let wrapper;
let store;
- describe('When the self monitor project has not been created', () => {
+ describe('When the self-monitor project has not been created', () => {
beforeEach(() => {
store = createStore({
projectEnabled: false,
@@ -35,7 +35,7 @@ describe('self monitor component', () => {
it('renders header text', () => {
wrapper = shallowMount(SelfMonitor, { store });
- expect(wrapper.find('.js-section-header').text()).toBe('Self monitoring');
+ expect(wrapper.find('.js-section-header').text()).toBe('Self-monitoring');
});
describe('expand/collapse button', () => {
@@ -53,7 +53,7 @@ describe('self monitor component', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.find('.js-section-sub-header').text()).toContain(
- 'Activate or deactivate instance self monitoring.',
+ 'Activate or deactivate instance self-monitoring.',
);
});
});
@@ -63,7 +63,7 @@ describe('self monitor component', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.vm.selfMonitoringFormText).toContain(
- 'Activate self monitoring to create a project to use to monitor the health of your instance.',
+ 'Activate self-monitoring to create a project to use to monitor the health of your instance.',
);
});
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 59ee87c4a02..21e63533c66 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -6,7 +6,7 @@ import * as actions from '~/self_monitor/store/actions';
import * as types from '~/self_monitor/store/mutation_types';
import createState from '~/self_monitor/store/state';
-describe('self monitor actions', () => {
+describe('self-monitor actions', () => {
let state;
let mock;
@@ -129,7 +129,7 @@ describe('self monitor actions', () => {
payload: {
actionName: 'viewSelfMonitorProject',
actionText: 'View project',
- message: 'Self monitoring project successfully created.',
+ message: 'Self-monitoring project successfully created.',
},
},
{ type: types.SET_SHOW_ALERT, payload: true },
@@ -236,7 +236,7 @@ describe('self monitor actions', () => {
payload: {
actionName: 'createProject',
actionText: 'Undo',
- message: 'Self monitoring project successfully deleted.',
+ message: 'Self-monitoring project successfully deleted.',
},
},
{ type: types.SET_SHOW_ALERT, payload: true },
diff --git a/spec/frontend/self_monitor/store/mutations_spec.js b/spec/frontend/self_monitor/store/mutations_spec.js
index 5282ae3b2f5..315450f3aef 100644
--- a/spec/frontend/self_monitor/store/mutations_spec.js
+++ b/spec/frontend/self_monitor/store/mutations_spec.js
@@ -1,7 +1,7 @@
import mutations from '~/self_monitor/store/mutations';
import createState from '~/self_monitor/store/state';
-describe('self monitoring mutations', () => {
+describe('self-monitoring mutations', () => {
let localState;
beforeEach(() => {
diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
new file mode 100644
index 00000000000..843ac1da4bb
--- /dev/null
+++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
@@ -0,0 +1,93 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
+import { __ } from '~/locale';
+import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
+import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
+
+describe('MilestoneDropdown component', () => {
+ let wrapper;
+
+ const propsData = {
+ attrWorkspacePath: 'full/path',
+ issuableType: IssuableType.Issue,
+ workspaceType: WorkspaceType.project,
+ };
+
+ const findHiddenInput = () => wrapper.find('input');
+ const findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(MilestoneDropdown, { propsData: { ...propsData, ...props } });
+ };
+
+ it('renders SidebarDropdown', () => {
+ createComponent();
+
+ expect(findSidebarDropdown().props()).toMatchObject({
+ attrWorkspacePath: propsData.attrWorkspacePath,
+ issuableAttribute: MilestoneDropdown.issuableAttribute,
+ issuableType: propsData.issuableType,
+ workspaceType: propsData.workspaceType,
+ });
+ });
+
+ it('renders hidden input', () => {
+ createComponent();
+
+ expect(findHiddenInput().attributes()).toEqual({
+ type: 'hidden',
+ name: 'update[milestone_id]',
+ value: undefined,
+ });
+ });
+
+ describe('when milestone ID and title is provided', () => {
+ it('is used in the dropdown and hidden input', () => {
+ const milestone = {
+ id: 'gid://gitlab/Milestone/52',
+ title: __('Milestone 52'),
+ };
+ createComponent({ milestoneId: milestone.id, milestoneTitle: milestone.title });
+
+ expect(findSidebarDropdown().props('currentAttribute')).toEqual(milestone);
+ expect(findHiddenInput().attributes('value')).toBe(
+ getIdFromGraphQLId(milestone.id).toString(),
+ );
+ });
+ });
+
+ describe('when SidebarDropdown emits `change` event', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when valid milestone is emitted', () => {
+ it('updates the hidden input value', async () => {
+ const milestone = {
+ id: 'gid://gitlab/Milestone/52',
+ title: __('Milestone 52'),
+ };
+
+ findSidebarDropdown().vm.$emit('change', milestone);
+ await nextTick();
+
+ expect(findHiddenInput().attributes('value')).toBe(
+ getIdFromGraphQLId(milestone.id).toString(),
+ );
+ });
+ });
+
+ describe('when null milestone is emitted', () => {
+ it('updates the hidden input value to `0`', async () => {
+ const milestone = { id: null };
+
+ findSidebarDropdown().vm.$emit('change', milestone);
+ await nextTick();
+
+ expect(findHiddenInput().attributes('value')).toBe('0');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js
new file mode 100644
index 00000000000..277ef6d9561
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import SidebarReviewersInputs from '~/sidebar/components/reviewers/sidebar_reviewers_inputs.vue';
+import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
+
+let wrapper;
+
+function factory() {
+ wrapper = shallowMount(SidebarReviewersInputs);
+}
+
+describe('Sidebar reviewers inputs component', () => {
+ it('renders hidden input', () => {
+ state.issuable.reviewers = {
+ nodes: [
+ {
+ id: 1,
+ avatarUrl: '',
+ name: 'root',
+ username: 'root',
+ mergeRequestInteraction: { canMerge: true },
+ },
+ {
+ id: 2,
+ avatarUrl: '',
+ name: 'root',
+ username: 'root',
+ mergeRequestInteraction: { canMerge: true },
+ },
+ ],
+ };
+
+ factory();
+
+ expect(wrapper.findAll('input[type="hidden"]').length).toBe(2);
+ });
+});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
new file mode 100644
index 00000000000..83bc8cf7002
--- /dev/null
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
@@ -0,0 +1,285 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlFormInput,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { IssuableType } from '~/issues/constants';
+import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
+import { IssuableAttributeType } from '~/sidebar/constants';
+import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import {
+ emptyProjectMilestonesResponse,
+ mockIssue,
+ mockProjectMilestonesResponse,
+ noCurrentMilestoneResponse,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('SidebarDropdown component', () => {
+ let wrapper;
+
+ const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } };
+ const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData });
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownText = () => wrapper.findComponent(GlDropdownText);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemWithText = (text) =>
+ findAllDropdownItems().wrappers.find((x) => x.text() === text);
+ const findAttributeItems = () => wrapper.findByTestId('milestone-items');
+ const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item');
+ const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
+
+ const toggleDropdown = async () => {
+ wrapper.vm.$refs.dropdown.show();
+ findDropdown().vm.$emit('show');
+
+ await nextTick();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ };
+
+ const createComponentWithApollo = ({
+ requestHandlers = [],
+ projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse),
+ currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse),
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ wrapper = mountExtended(SidebarDropdown, {
+ apolloProvider: createMockApollo([
+ [projectMilestonesQuery, projectMilestonesSpy],
+ [projectIssueMilestoneQuery, currentMilestoneSpy],
+ ...requestHandlers,
+ ]),
+ propsData: {
+ attrWorkspacePath: mockIssue.projectPath,
+ currentAttribute: {},
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ },
+ attachTo: document.body,
+ });
+ };
+
+ const createComponent = ({
+ props = {},
+ data = {},
+ mutationPromise = mutationSuccess,
+ queries = {},
+ } = {}) => {
+ wrapper = mountExtended(SidebarDropdown, {
+ propsData: {
+ attrWorkspacePath: mockIssue.projectPath,
+ currentAttribute: {},
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ mocks: {
+ $apollo: {
+ mutate: mutationPromise(),
+ queries: {
+ currentAttribute: { loading: false },
+ attributesList: { loading: false },
+ ...queries,
+ },
+ },
+ },
+ });
+ };
+
+ describe('when a user can edit', () => {
+ describe('when user is editing', () => {
+ describe('when rendering the dropdown', () => {
+ it('shows a loading spinner while fetching a list of attributes', async () => {
+ createComponent({
+ queries: {
+ attributesList: { loading: true },
+ },
+ });
+
+ await toggleDropdown();
+
+ expect(findLoadingIconDropdown().exists()).toBe(true);
+ });
+
+ describe('GlDropdownItem with the right title and id', () => {
+ const id = 'id';
+ const title = 'title';
+
+ beforeEach(async () => {
+ createComponent({
+ props: { currentAttribute: { id, title } },
+ data: { attributesList: [{ id, title }] },
+ });
+
+ await toggleDropdown();
+ });
+
+ it('does not show a loading spinner', () => {
+ expect(findLoadingIconDropdown().exists()).toBe(false);
+ });
+
+ it('renders title $title', () => {
+ expect(findDropdownItemWithText(title).exists()).toBe(true);
+ });
+
+ it('checks the correct dropdown item', () => {
+ expect(
+ findAllDropdownItems()
+ .filter((w) => w.props('isChecked') === true)
+ .at(0)
+ .text(),
+ ).toBe(title);
+ });
+ });
+
+ describe('when no data is assigned', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await toggleDropdown();
+ });
+
+ it('finds GlDropdownItem with "No milestone"', () => {
+ expect(findNoAttributeItem().text()).toBe('No milestone');
+ });
+
+ it('"No milestone" is checked', () => {
+ expect(findAllDropdownItems('No milestone').at(0).props('isChecked')).toBe(true);
+ });
+
+ it('does not render any dropdown item', () => {
+ expect(findAttributeItems().exists()).toBe(false);
+ });
+ });
+
+ describe('when clicking on dropdown item', () => {
+ describe('when currentAttribute is equal to attribute id', () => {
+ it('does not call setIssueAttribute mutation', async () => {
+ createComponent({
+ props: { currentAttribute: { id: 'id', title: 'title' } },
+ data: { attributesList: [{ id: 'id', title: 'title' }] },
+ });
+
+ await toggleDropdown();
+
+ findDropdownItemWithText('title').vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+ });
+
+ describe('when a user is searching', () => {
+ describe('when search result is not found', () => {
+ describe('when milestone', () => {
+ it('renders "No milestone found"', async () => {
+ createComponent();
+
+ await toggleDropdown();
+
+ findSearchBox().vm.$emit('input', 'non existing milestones');
+ await nextTick();
+
+ expect(findDropdownText().text()).toBe('No milestone found');
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('with mock apollo', () => {
+ describe("when issuable type is 'issue'", () => {
+ describe('when dropdown is expanded and user can edit', () => {
+ it('renders the dropdown on clicking edit', async () => {
+ createComponentWithApollo();
+
+ await toggleDropdown();
+
+ expect(findDropdown().isVisible()).toBe(true);
+ });
+
+ it('focuses on the input when dropdown is shown', async () => {
+ createComponentWithApollo();
+
+ await toggleDropdown();
+
+ expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element);
+ });
+
+ describe('milestones', () => {
+ it('should call createAlert if milestones query fails', async () => {
+ createComponentWithApollo({
+ projectMilestonesSpy: jest.fn().mockRejectedValue(new Error()),
+ });
+
+ await toggleDropdown();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: wrapper.vm.i18n.listFetchError,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+
+ it('only fetches attributes when dropdown is opened', async () => {
+ const projectMilestonesSpy = jest
+ .fn()
+ .mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ createComponentWithApollo({ projectMilestonesSpy });
+
+ expect(projectMilestonesSpy).not.toHaveBeenCalled();
+
+ await toggleDropdown();
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
+ fullPath: mockIssue.projectPath,
+ state: 'active',
+ title: '',
+ });
+ });
+
+ describe('when a user is searching', () => {
+ it('sends a projectMilestones query with the entered search term "foo"', async () => {
+ const mockSearchTerm = 'foobar';
+ const projectMilestonesSpy = jest
+ .fn()
+ .mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ createComponentWithApollo({ projectMilestonesSpy });
+
+ await toggleDropdown();
+
+ findSearchBox().vm.$emit('input', mockSearchTerm);
+ await nextTick();
+ jest.runOnlyPendingTimers(); // Account for debouncing
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
+ fullPath: mockIssue.projectPath,
+ state: 'active',
+ title: mockSearchTerm,
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 8ab4d8ea051..cf5e220a705 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -1,12 +1,4 @@
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlLink,
- GlSearchBoxByType,
- GlFormInput,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlDropdown, GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -19,6 +11,7 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
+import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { IssuableAttributeType } from '~/sidebar/constants';
@@ -32,7 +25,6 @@ import {
noCurrentMilestoneResponse,
mockMilestoneMutationResponse,
mockMilestone2,
- emptyProjectMilestonesResponse,
} from '../mock_data';
jest.mock('~/flash');
@@ -55,20 +47,11 @@ describe('SidebarDropdownWidget', () => {
const findGlLink = () => wrapper.findComponent(GlLink);
const findDateTooltip = () => getBinding(findGlLink().element, 'gl-tooltip');
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownText = () => wrapper.findComponent(GlDropdownText);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findDropdownItemWithText = (text) =>
- findAllDropdownItems().wrappers.find((x) => x.text() === text);
-
+ const findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown);
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]');
const findEditableLoadingIcon = () => findSidebarEditableItem().findComponent(GlLoadingIcon);
- const findAttributeItems = () => wrapper.findByTestId('milestone-items');
const findSelectedAttribute = () => wrapper.findByTestId('select-milestone');
- const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item');
- const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
const waitForDropdown = async () => {
// BDropdown first changes its `visible` property
@@ -167,6 +150,8 @@ describe('SidebarDropdownWidget', () => {
}),
);
+ wrapper.vm.$refs.dropdown.show = jest.fn();
+
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
@@ -261,86 +246,7 @@ describe('SidebarDropdownWidget', () => {
describe('when a user can edit', () => {
describe('when user is editing', () => {
describe('when rendering the dropdown', () => {
- it('shows a loading spinner while fetching a list of attributes', async () => {
- createComponent({
- queries: {
- attributesList: { loading: true },
- },
- });
-
- await toggleDropdown();
-
- expect(findLoadingIconDropdown().exists()).toBe(true);
- });
-
- describe('GlDropdownItem with the right title and id', () => {
- const id = 'id';
- const title = 'title';
-
- beforeEach(async () => {
- createComponent({
- data: { attributesList: [{ id, title }], currentAttribute: { id, title } },
- });
-
- await toggleDropdown();
- });
-
- it('does not show a loading spinner', () => {
- expect(findLoadingIconDropdown().exists()).toBe(false);
- });
-
- it('renders title $title', () => {
- expect(findDropdownItemWithText(title).exists()).toBe(true);
- });
-
- it('checks the correct dropdown item', () => {
- expect(
- findAllDropdownItems()
- .filter((w) => w.props('isChecked') === true)
- .at(0)
- .text(),
- ).toBe(title);
- });
- });
-
- describe('when no data is assigned', () => {
- beforeEach(async () => {
- createComponent();
-
- await toggleDropdown();
- });
-
- it('finds GlDropdownItem with "No milestone"', () => {
- expect(findNoAttributeItem().text()).toBe('No milestone');
- });
-
- it('"No milestone" is checked', () => {
- expect(findNoAttributeItem().props('isChecked')).toBe(true);
- });
-
- it('does not render any dropdown item', () => {
- expect(findAttributeItems().exists()).toBe(false);
- });
- });
-
describe('when clicking on dropdown item', () => {
- describe('when currentAttribute is equal to attribute id', () => {
- it('does not call setIssueAttribute mutation', async () => {
- createComponent({
- data: {
- attributesList: [{ id: 'id', title: 'title' }],
- currentAttribute: { id: 'id', title: 'title' },
- },
- });
-
- await toggleDropdown();
-
- findDropdownItemWithText('title').vm.$emit('click');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
- });
- });
-
describe('when currentAttribute is not equal to attribute id', () => {
describe('when error', () => {
const bootstrapComponent = (mutationResp) => {
@@ -350,7 +256,7 @@ describe('SidebarDropdownWidget', () => {
{ id: '123', title: '123' },
{ id: 'id', title: 'title' },
],
- currentAttribute: '123',
+ currentAttribute: { id: '123' },
},
mutationPromise: mutationResp,
});
@@ -366,7 +272,7 @@ describe('SidebarDropdownWidget', () => {
await toggleDropdown();
- findDropdownItemWithText('title').vm.$emit('click');
+ findSidebarDropdown().vm.$emit('change', { id: 'error' });
});
it(`calls createAlert with "${expectedMsg}"`, async () => {
@@ -382,24 +288,6 @@ describe('SidebarDropdownWidget', () => {
});
});
});
-
- describe('when a user is searching', () => {
- describe('when search result is not found', () => {
- describe('when milestone', () => {
- it('renders "No milestone found"', async () => {
- createComponent();
-
- await toggleDropdown();
-
- findSearchBox().vm.$emit('input', 'non existing milestones');
-
- await nextTick();
-
- expect(findDropdownText().text()).toBe('No milestone found');
- });
- });
- });
- });
});
});
@@ -424,18 +312,10 @@ describe('SidebarDropdownWidget', () => {
await clickEdit();
});
- it('renders the dropdown on clicking edit', async () => {
- expect(findDropdown().isVisible()).toBe(true);
- });
-
- it('focuses on the input when dropdown is shown', async () => {
- expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element);
- });
-
describe('when currentAttribute is not equal to attribute id', () => {
describe('when update is successful', () => {
it('calls setIssueAttribute mutation', () => {
- findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
+ findSidebarDropdown().vm.$emit('change', { id: mockMilestone2.id });
expect(milestoneMutationSpy).toHaveBeenCalledWith({
iid: mockIssue.iid,
@@ -443,72 +323,6 @@ describe('SidebarDropdownWidget', () => {
fullPath: mockIssue.projectPath,
});
});
-
- it('sets the value returned from the mutation to currentAttribute', async () => {
- findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
- await nextTick();
- expect(findSelectedAttribute().text()).toBe(mockMilestone2.title);
- });
- });
- });
-
- describe('milestones', () => {
- let projectMilestonesSpy;
-
- it('should call createAlert if milestones query fails', async () => {
- await createComponentWithApollo({
- projectMilestonesSpy: jest.fn().mockRejectedValue(error),
- });
-
- await clickEdit();
-
- expect(createAlert).toHaveBeenCalledWith({
- message: wrapper.vm.i18n.listFetchError,
- captureError: true,
- error: expect.any(Error),
- });
- });
-
- it('only fetches attributes when dropdown is opened', async () => {
- projectMilestonesSpy = jest.fn().mockResolvedValueOnce(emptyProjectMilestonesResponse);
- await createComponentWithApollo({ projectMilestonesSpy });
-
- expect(projectMilestonesSpy).not.toHaveBeenCalled();
-
- await clickEdit();
-
- expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
- fullPath: mockIssue.projectPath,
- state: 'active',
- title: '',
- });
- });
-
- describe('when a user is searching', () => {
- const mockSearchTerm = 'foobar';
-
- beforeEach(async () => {
- projectMilestonesSpy = jest
- .fn()
- .mockResolvedValueOnce(emptyProjectMilestonesResponse);
- await createComponentWithApollo({ projectMilestonesSpy });
-
- await clickEdit();
- });
-
- it('sends a projectMilestones query with the entered search term "foo"', async () => {
- findSearchBox().vm.$emit('input', mockSearchTerm);
- await nextTick();
-
- // Account for debouncing
- jest.runAllTimers();
-
- expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
- fullPath: mockIssue.projectPath,
- state: 'active',
- title: mockSearchTerm,
- });
- });
});
});
});
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 6e2908e659f..2eed1e30d0d 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -38,6 +38,10 @@ export const projectsWithScope = {
id: '2',
fullPath: 'root/332268-test',
name: 'root/332268-test',
+ namespace: {
+ id: '1234',
+ fullPath: 'root',
+ },
},
],
},
@@ -68,6 +72,10 @@ export const mockProjects = [
{
id: '1',
name: 'merge-train-stuff',
+ namespace: {
+ id: '1235',
+ fullPath: 'root',
+ },
fullPath: 'root/merge-train-stuff',
isLocked: false,
__typename: 'Project',
@@ -75,6 +83,10 @@ export const mockProjects = [
{
id: '2',
name: 'ci-project',
+ namespace: {
+ id: '1236',
+ fullPath: 'root',
+ },
fullPath: 'root/ci-project',
isLocked: true,
__typename: 'Project',
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
index c55ac32b6a6..ea1d9db515a 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -19,7 +19,8 @@ import {
} from './mock_data';
const projectPath = 'root/my-repo';
-const error = new Error('Error');
+const message = 'An error occurred';
+const error = new Error(message);
Vue.use(VueApollo);
@@ -144,7 +145,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message });
});
});
@@ -187,7 +188,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message });
});
});
});
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index 3bda0d0b530..0fa1a2453f7 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -1,5 +1,5 @@
import { GlTable, GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import TokenProjectsTable from '~/token_access/components/token_projects_table.vue';
import { mockProjects } from './mock_data';
@@ -7,7 +7,7 @@ describe('Token projects table', () => {
let wrapper;
const createComponent = () => {
- wrapper = mount(TokenProjectsTable, {
+ wrapper = mountExtended(TokenProjectsTable, {
provide: {
fullPath: 'root/ci-project',
},
@@ -18,9 +18,11 @@ describe('Token projects table', () => {
};
const findTable = () => wrapper.findComponent(GlTable);
- const findAllTableRows = () => wrapper.findAll('[data-testid="projects-token-table-row"]');
const findDeleteProjectBtn = () => wrapper.findComponent(GlButton);
const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton);
+ const findAllTableRows = () => wrapper.findAllByTestId('projects-token-table-row');
+ const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name');
+ const findProjectNamespaceCell = () => wrapper.findByTestId('token-access-project-namespace');
beforeEach(() => {
createComponent();
@@ -48,4 +50,9 @@ describe('Token projects table', () => {
// currently two mock projects with one being a locked project
expect(findAllDeleteProjectBtn()).toHaveLength(1);
});
+
+ it('displays project and namespace cells', () => {
+ expect(findProjectNameCell().text()).toBe('merge-train-stuff');
+ expect(findProjectNamespaceCell().text()).toBe('root');
+ });
});
diff --git a/spec/frontend/users_select/utils_spec.js b/spec/frontend/users_select/utils_spec.js
index a09935d8a04..7a080ddaf0f 100644
--- a/spec/frontend/users_select/utils_spec.js
+++ b/spec/frontend/users_select/utils_spec.js
@@ -1,21 +1,10 @@
-import $ from 'jquery';
-import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from '~/users_select/utils';
+import { getAjaxUsersSelectParams } from '~/users_select/utils';
const options = {
fooBar: 'baz',
activeUserId: 1,
};
-describe('getAjaxUsersSelectOptions', () => {
- it('returns options built from select data attributes', () => {
- const $select = $('<select />', { 'data-foo-bar': 'baz', 'data-user-id': 1 });
-
- expect(
- getAjaxUsersSelectOptions($select, { fooBar: 'fooBar', activeUserId: 'user-id' }),
- ).toEqual(options);
- });
-});
-
describe('getAjaxUsersSelectParams', () => {
it('returns query parameters built from provided options', () => {
expect(
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
index 4e3e918f7fb..8dadb0c65d0 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
@@ -20,7 +20,7 @@ describe('MrWidgetContainer', () => {
it('has layout', () => {
factory();
- expect(wrapper.classes()).toContain('mr-widget-heading');
+ expect(wrapper.classes()).toEqual(['mr-section-container', 'mr-widget-workflow']);
expect(wrapper.find('.mr-widget-content').exists()).toBe(true);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
index 7f0173b7445..144e176b0f0 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
@@ -222,6 +222,7 @@ describe('MRWidgetPipeline', () => {
beforeEach(() => {
({ pipeline } = JSON.parse(JSON.stringify(mockData)));
+ pipeline.details.event_type_name = 'Pipeline';
pipeline.details.name = 'Pipeline';
pipeline.merge_request_event_type = undefined;
pipeline.ref.tag = false;
@@ -263,6 +264,7 @@ describe('MRWidgetPipeline', () => {
describe('for a detached merge request pipeline', () => {
it('renders a pipeline widget that reads "Merge request pipeline <ID> <status> for <SHA>"', () => {
+ pipeline.details.event_type_name = 'Merge request pipeline';
pipeline.details.name = 'Merge request pipeline';
pipeline.merge_request_event_type = 'detached';
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
index 05c259de370..7b52773e92d 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
@@ -8,7 +8,7 @@ jest.mock('~/vue_shared/plugins/global_toast');
let wrapper;
-function createWrapper(propsData, mergeRequestWidgetGraphql) {
+function createWrapper(propsData) {
wrapper = mount(WidgetRebase, {
propsData,
data() {
@@ -22,7 +22,6 @@ function createWrapper(propsData, mergeRequestWidgetGraphql) {
},
};
},
- provide: { glFeatures: { mergeRequestWidgetGraphql } },
mocks: {
$apollo: {
queries: {
@@ -43,276 +42,244 @@ describe('Merge request widget rebase component', () => {
wrapper.destroy();
wrapper = null;
});
-
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
- describe('while rebasing', () => {
- it('should show progress message', () => {
- createWrapper(
- {
- mr: { rebaseInProgress: true },
- service: {},
- },
- mergeRequestWidgetGraphql,
- );
-
- expect(findRebaseMessageText()).toContain('Rebase in progress');
- });
+ describe('while rebasing', () => {
+ it('should show progress message', () => {
+ createWrapper({
+ mr: { rebaseInProgress: true },
+ service: {},
});
- describe('with permissions', () => {
- const rebaseMock = jest.fn().mockResolvedValue();
- const pollMock = jest.fn().mockResolvedValue({});
+ expect(findRebaseMessageText()).toContain('Rebase in progress');
+ });
+ });
+
+ describe('with permissions', () => {
+ const rebaseMock = jest.fn().mockResolvedValue();
+ const pollMock = jest.fn().mockResolvedValue({});
- it('renders the warning message', () => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
+ it('renders the warning message', () => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ });
- const text = findRebaseMessageText();
+ const text = findRebaseMessageText();
- expect(text).toContain('Merge blocked');
- expect(text.replace(/\s\s+/g, ' ')).toContain(
- 'the source branch must be rebased onto the target branch',
- );
- });
+ expect(text).toContain('Merge blocked');
+ expect(text.replace(/\s\s+/g, ' ')).toContain(
+ 'the source branch must be rebased onto the target branch',
+ );
+ });
- it('renders an error message when rebasing has failed', async () => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
+ it('renders an error message when rebasing has failed', async () => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ });
+
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({ rebasingError: 'Something went wrong!' });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ rebasingError: 'Something went wrong!' });
+ await nextTick();
+ expect(findRebaseMessageText()).toContain('Something went wrong!');
+ });
- await nextTick();
- expect(findRebaseMessageText()).toContain('Something went wrong!');
+ describe('Rebase buttons', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
});
+ });
- describe('Rebase buttons', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ it('renders both buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
- it('renders both buttons', () => {
- expect(findRebaseWithoutCiButton().exists()).toBe(true);
- expect(findStandardRebaseButton().exists()).toBe(true);
- });
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
- it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ await nextTick();
- await nextTick();
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
- });
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
- it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
- findRebaseWithoutCiButton().vm.$emit('click');
+ await nextTick();
- await nextTick();
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
- });
+ describe('Rebase when pipelines must succeed is enabled', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
});
+ });
- describe('Rebase when pipelines must succeed is enabled', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ it('renders only the rebase button', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(false);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
- it('renders only the rebase button', () => {
- expect(findRebaseWithoutCiButton().exists()).toBe(false);
- expect(findStandardRebaseButton().exists()).toBe(true);
- });
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
- it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ await nextTick();
- await nextTick();
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+ });
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
- });
+ describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
});
+ });
- describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- allowMergeOnSkippedPipeline: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ it('renders both rebase buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
- it('renders both rebase buttons', () => {
- expect(findRebaseWithoutCiButton().exists()).toBe(true);
- expect(findStandardRebaseButton().exists()).toBe(true);
- });
+ await nextTick();
- it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
- await nextTick();
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
- });
+ await nextTick();
- it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
- findRebaseWithoutCiButton().vm.$emit('click');
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+ });
- await nextTick();
+ describe('without permissions', () => {
+ const exampleTargetBranch = 'fake-branch-to-test-with';
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
- });
+ describe('UI text', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ },
+ service: {},
});
});
- describe('without permissions', () => {
- const exampleTargetBranch = 'fake-branch-to-test-with';
-
- describe('UI text', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: false,
- targetBranch: exampleTargetBranch,
- },
- service: {},
- },
- mergeRequestWidgetGraphql,
- );
- });
-
- it('renders a message explaining user does not have permissions', () => {
- const text = findRebaseMessageText();
-
- expect(text).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
- expect(text).toContain('the source branch must be rebased');
- });
-
- it('renders the correct target branch name', () => {
- const elem = findRebaseMessage();
-
- expect(elem.text()).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
- });
- });
+ it('renders a message explaining user does not have permissions', () => {
+ const text = findRebaseMessageText();
- it('does render the "Rebase without pipeline" button', () => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: false,
- targetBranch: exampleTargetBranch,
- },
- service: {},
- },
- mergeRequestWidgetGraphql,
- );
+ expect(text).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ expect(text).toContain('the source branch must be rebased');
+ });
- expect(findRebaseWithoutCiButton().exists()).toBe(true);
- });
+ it('renders the correct target branch name', () => {
+ const elem = findRebaseMessage();
+
+ expect(elem.text()).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ });
+ });
+
+ it('does render the "Rebase without pipeline" button', () => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ },
+ service: {},
});
- describe('methods', () => {
- it('checkRebaseStatus', async () => {
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- createWrapper(
- {
- mr: {},
- service: {
- rebase() {
- return Promise.resolve();
- },
- poll() {
- return Promise.resolve({
- data: {
- rebase_in_progress: false,
- should_be_rebased: false,
- merge_error: null,
- },
- });
- },
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ });
+ });
+
+ describe('methods', () => {
+ it('checkRebaseStatus', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ createWrapper({
+ mr: {},
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ should_be_rebased: false,
+ merge_error: null,
},
- },
- mergeRequestWidgetGraphql,
- );
+ });
+ },
+ },
+ });
- wrapper.vm.rebase();
+ wrapper.vm.rebase();
- // Wait for the rebase request
- await nextTick();
- // Wait for the polling request
- await nextTick();
- // Wait for the eventHub to be called
- await nextTick();
+ // Wait for the rebase request
+ await nextTick();
+ // Wait for the polling request
+ await nextTick();
+ // Wait for the eventHub to be called
+ await nextTick();
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
- expect(toast).toHaveBeenCalledWith('Rebase completed');
- });
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
+ expect(toast).toHaveBeenCalledWith('Rebase completed');
});
});
});
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 5f383c468d8..bd40a968392 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
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
+exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
<div
- class="mr-widget-body media"
+ class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
>
<div
class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
@@ -44,210 +44,18 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
class="gl-display-flex gl-w-full"
>
<div
- class="media-body gl-display-flex"
+ class="media-body gl-display-flex gl-align-items-center"
>
<h4
class="gl-mr-3"
data-testid="statusText"
>
- Set by
- <a
- class="author-link inline"
- >
- <img
- class="avatar avatar-inline s16"
- src="no_avatar.png"
- />
-
- <span
- class="author"
- >
-
- </span>
- </a>
- to be merged automatically when the pipeline succeeds
- </h4>
-
- <div
- class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
- >
- <div
- 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"
- lazy=""
- no-caret=""
- title="Options"
- >
- <!---->
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="dropdown-icon gl-icon s16"
- data-testid="ellipsis_v-icon"
- role="img"
- >
- <use
- href="#ellipsis_v"
- />
- </svg>
-
- <span
- class="gl-new-dropdown-button-text gl-sr-only"
- >
-
- </span>
-
- <svg
- aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
- data-testid="chevron-down-icon"
- role="img"
- >
- <use
- href="#chevron-down"
- />
- </svg>
- </button>
- <ul
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
- </div>
-
- <button
- class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Cancel auto-merge
-
- </span>
- </button>
- </div>
- </div>
- </div>
-
- <div
- class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
- >
- <button
- class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
- title="Collapse merge details"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="chevron-lg-up-icon"
- role="img"
- >
- <use
- href="#chevron-lg-up"
- />
- </svg>
-
- <!---->
- </button>
- </div>
- </div>
-</div>
-`;
-
-exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
-<div
- class="mr-widget-body media"
->
- <div
- class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
- >
- <div
- class="gl-display-flex gl-m-auto"
- >
- <div
- class="gl-mr-3 gl-p-2 gl-m-0! gl-text-blue-500 gl-w-6 gl-p-2"
- >
- <div
- class="gl-rounded-full gl-relative gl-display-flex mr-widget-extension-icon"
- >
- <div
- class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto"
- >
- <div
- class="gl-display-flex gl-m-auto gl-translate-y-n50"
- >
- <svg
- aria-label="Scheduled "
- class="gl-display-block gl-icon s12"
- data-qa-selector="status_scheduled_icon"
- data-testid="status-scheduled-icon"
- role="img"
- >
- <use
- href="#status-scheduled"
- />
- </svg>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-w-full"
- >
- <div
- class="media-body gl-display-flex"
- >
-
- <h4
- class="gl-mr-3"
- data-testid="statusText"
- >
- Set by
- <a
- class="author-link inline"
- >
- <img
- class="avatar avatar-inline s16"
- src="no_avatar.png"
- />
-
- <span
- class="author"
- >
-
- </span>
- </a>
- to be merged automatically when the pipeline succeeds
+ Set by to be merged automatically when the pipeline succeeds
</h4>
<div
- class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
+ class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3"
>
<div
class="gl-display-flex gl-align-items-flex-start"
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index d85574262fe..8eeba4d6274 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import MergeChecksFailed from '~/vue_merge_request_widget/components/states/merge_checks_failed.vue';
+import { DETAILED_MERGE_STATUS } from '~/vue_merge_request_widget/constants';
let wrapper;
@@ -15,9 +16,10 @@ describe('Merge request widget merge checks failed state component', () => {
});
it.each`
- mrState | displayText
- ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
- ${{ detailedMergeStatus: 'BLOCKED_STATUS' }} | ${'blockingMergeRequests'}
+ mrState | displayText
+ ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
+ ${{ detailedMergeStatus: DETAILED_MERGE_STATUS.BLOCKED_STATUS }} | ${'blockingMergeRequests'}
+ ${{ detailedMergeStatus: DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS }} | ${'externalStatusChecksFailed'}
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 28182793683..5b9f30dfb86 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -9,7 +9,6 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
let wrapper;
-let mergeRequestWidgetGraphqlEnabled = false;
function convertPropsToGraphqlState(props) {
return {
@@ -30,12 +29,6 @@ function convertPropsToGraphqlState(props) {
}
function factory(propsData, stateOverride = {}) {
- let state = {};
-
- if (mergeRequestWidgetGraphqlEnabled) {
- state = { ...convertPropsToGraphqlState(propsData), ...stateOverride };
- }
-
wrapper = extendedWrapper(
mount(autoMergeEnabledComponent, {
propsData: {
@@ -43,9 +36,8 @@ function factory(propsData, stateOverride = {}) {
service: new MRWidgetService({}),
},
data() {
- return { state };
+ return { ...convertPropsToGraphqlState(propsData), ...stateOverride };
},
- provide: { glFeatures: { mergeRequestWidgetGraphql: mergeRequestWidgetGraphqlEnabled } },
mocks: {
$apollo: {
queries: {
@@ -95,130 +87,88 @@ describe('MRWidgetAutoMergeEnabled', () => {
wrapper = null;
});
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
- beforeEach(() => {
- mergeRequestWidgetGraphqlEnabled = mergeRequestWidgetGraphql;
+ describe('computed', () => {
+ describe('cancelButtonText', () => {
+ it('should return "Cancel" if MWPS is selected', () => {
+ factory({
+ ...defaultMrProps(),
+ autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ });
+
+ expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe('Cancel auto-merge');
});
+ });
+ });
- describe('computed', () => {
- describe('cancelButtonText', () => {
- it('should return "Cancel" if MWPS is selected', () => {
- factory({
- ...defaultMrProps(),
- autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ describe('methods', () => {
+ describe('cancelAutomaticMerge', () => {
+ it('should set flag and call service then tell main component to update the widget with data', async () => {
+ factory({
+ ...defaultMrProps(),
+ });
+ const mrObj = {
+ is_new_mr_data: true,
+ };
+ jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue(
+ new Promise((resolve) => {
+ resolve({
+ data: mrObj,
});
+ }),
+ );
- expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe(
- 'Cancel auto-merge',
- );
- });
- });
- });
+ wrapper.vm.cancelAutomaticMerge();
- describe('methods', () => {
- describe('cancelAutomaticMerge', () => {
- it('should set flag and call service then tell main component to update the widget with data', async () => {
- factory({
- ...defaultMrProps(),
- });
- const mrObj = {
- is_new_mr_data: true,
- };
- jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue(
- new Promise((resolve) => {
- resolve({
- data: mrObj,
- });
- }),
- );
-
- wrapper.vm.cancelAutomaticMerge();
-
- await waitForPromises();
-
- expect(wrapper.vm.isCancellingAutoMerge).toBe(true);
- if (mergeRequestWidgetGraphql) {
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- } else {
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- }
- });
- });
+ await waitForPromises();
- describe('removeSourceBranch', () => {
- it('should set flag and call service then request main component to update the widget', async () => {
- factory({
- ...defaultMrProps(),
- });
- jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(
- Promise.resolve({
- data: {
- status: MWPS_MERGE_STRATEGY,
- },
- }),
- );
-
- wrapper.vm.removeSourceBranch();
-
- await waitForPromises();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
- sha,
- auto_merge_strategy: MWPS_MERGE_STRATEGY,
- should_remove_source_branch: true,
- });
- });
- });
+ expect(wrapper.vm.isCancellingAutoMerge).toBe(true);
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
});
+ });
+ });
- describe('template', () => {
- it('should have correct elements', () => {
- factory({
- ...defaultMrProps(),
- });
+ describe('template', () => {
+ it('should have correct elements', () => {
+ factory({
+ ...defaultMrProps(),
+ });
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
+ });
- it('should disable cancel auto merge button when the action is in progress', async () => {
- factory({
- ...defaultMrProps(),
- });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isCancellingAutoMerge: true,
- });
+ it('should disable cancel auto merge button when the action is in progress', async () => {
+ factory({
+ ...defaultMrProps(),
+ });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({
+ isCancellingAutoMerge: true,
+ });
- await nextTick();
+ await nextTick();
- expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true);
- });
+ expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true);
+ });
- it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
- factory({
- ...defaultMrProps(),
- autoMergeStrategy: MWPS_MERGE_STRATEGY,
- });
+ it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
+ factory({
+ ...defaultMrProps(),
+ autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ });
- expect(getStatusText()).toContain(
- 'to be merged automatically when the pipeline succeeds',
- );
- });
+ expect(getStatusText()).toContain('to be merged automatically when the pipeline succeeds');
+ });
- it('should render the cancel button as "Cancel" if MWPS is selected', () => {
- factory({
- ...defaultMrProps(),
- autoMergeStrategy: MWPS_MERGE_STRATEGY,
- });
+ it('should render the cancel button as "Cancel" if MWPS is selected', () => {
+ factory({
+ ...defaultMrProps(),
+ autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ });
- const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
+ const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
- expect(cancelButtonText).toBe('Cancel auto-merge');
- });
- });
+ expect(cancelButtonText).toBe('Cancel auto-merge');
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
index 398a3912882..826f708069c 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -9,18 +9,11 @@ describe('MRWidgetAutoMergeFailed', () => {
const mergeError = 'This is the merge error';
const findButton = () => wrapper.findComponent(GlButton);
- const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
+ const createComponent = (props = {}) => {
wrapper = mount(AutoMergeFailedComponent, {
propsData: { ...props },
data() {
- if (mergeRequestWidgetGraphql) {
- return { mergeError: props.mr?.mergeError };
- }
-
- return {};
- },
- provide: {
- glFeatures: { mergeRequestWidgetGraphql },
+ return { mergeError: props.mr?.mergeError };
},
});
};
@@ -29,40 +22,33 @@ describe('MRWidgetAutoMergeFailed', () => {
wrapper.destroy();
});
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
- beforeEach(() => {
- createComponent(
- {
- mr: { mergeError },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ beforeEach(() => {
+ createComponent({
+ mr: { mergeError },
+ });
+ });
- it('renders failed message', () => {
- expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
- });
+ it('renders failed message', () => {
+ expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
+ });
- it('renders merge error provided', () => {
- expect(wrapper.text()).toContain(mergeError);
- });
+ it('renders merge error provided', () => {
+ expect(wrapper.text()).toContain(mergeError);
+ });
- it('render refresh button', () => {
- expect(findButton().text()).toBe('Refresh');
- });
+ it('render refresh button', () => {
+ expect(findButton().text()).toBe('Refresh');
+ });
- it('emits event and shows loading icon when button is clicked', async () => {
- jest.spyOn(eventHub, '$emit');
- findButton().vm.$emit('click');
+ it('emits event and shows loading icon when button is clicked', async () => {
+ jest.spyOn(eventHub, '$emit');
+ findButton().vm.$emit('click');
- expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
+ expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
- await nextTick();
+ await nextTick();
- expect(findButton().attributes('disabled')).toBe('disabled');
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
+ expect(findButton().attributes('disabled')).toBe('disabled');
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
index 7a9fd5b002d..a16e4d4a6ea 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
@@ -7,7 +7,6 @@ import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_
describe('MRWidgetConflicts', () => {
let wrapper;
- let mergeRequestWidgetGraphql = null;
const path = '/conflicts';
const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button');
@@ -25,10 +24,17 @@ describe('MRWidgetConflicts', () => {
wrapper = extendedWrapper(
mount(ConflictsComponent, {
propsData,
- provide: {
- glFeatures: {
- mergeRequestWidgetGraphql,
- },
+ data() {
+ return {
+ userPermissions: {
+ canMerge: propsData.mr.canMerge,
+ pushToSourceBranch: propsData.mr.canPushToSourceBranch,
+ },
+ state: {
+ shouldBeRebased: propsData.mr.shouldBeRebased,
+ sourceBranchProtected: propsData.mr.sourceBranchProtected,
+ },
+ };
},
mocks: {
$apollo: {
@@ -41,212 +47,188 @@ describe('MRWidgetConflicts', () => {
}),
);
- if (mergeRequestWidgetGraphql) {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- userPermissions: {
- canMerge: propsData.mr.canMerge,
- pushToSourceBranch: propsData.mr.canPushToSourceBranch,
- },
- stateData: {
- shouldBeRebased: propsData.mr.shouldBeRebased,
- sourceBranchProtected: propsData.mr.sourceBranchProtected,
- },
- });
- }
-
await nextTick();
}
afterEach(() => {
- mergeRequestWidgetGraphql = null;
wrapper.destroy();
});
- [false, true].forEach((featureEnabled) => {
- describe(`with GraphQL feature flag ${featureEnabled ? 'enabled' : 'disabled'}`, () => {
- beforeEach(() => {
- mergeRequestWidgetGraphql = featureEnabled;
+ // There are two permissions we need to consider:
+ //
+ // 1. Is the user allowed to merge to the target branch?
+ // 2. Is the user allowed to push to the source branch?
+ //
+ // This yields 4 possible permutations that we need to test, and
+ // we test them below. A user who can push to the source
+ // branch should be allowed to resolve conflicts. This is
+ // consistent with what the backend does.
+ describe('when allowed to merge but not allowed to push to source branch', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: false,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
});
+ });
- // There are two permissions we need to consider:
- //
- // 1. Is the user allowed to merge to the target branch?
- // 2. Is the user allowed to push to the source branch?
- //
- // This yields 4 possible permutations that we need to test, and
- // we test them below. A user who can push to the source
- // branch should be allowed to resolve conflicts. This is
- // consistent with what the backend does.
- describe('when allowed to merge but not allowed to push to source branch', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: false,
- conflictResolutionPath: path,
- conflictsDocsPath: '',
- },
- });
- });
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(wrapper.text()).toContain(mergeConflictsText);
+ expect(wrapper.text()).not.toContain(userCannotMergeText);
+ });
- it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
- });
+ it('should not allow you to resolve the conflicts', () => {
+ expect(wrapper.text()).not.toContain(resolveConflictsBtnText);
+ });
- it('should not allow you to resolve the conflicts', () => {
- expect(wrapper.text()).not.toContain(resolveConflictsBtnText);
- });
+ it('should have merge buttons', () => {
+ expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
+ });
+ });
- it('should have merge buttons', () => {
- expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
- });
+ describe('when not allowed to merge but allowed to push to source branch', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
});
+ });
- describe('when not allowed to merge but allowed to push to source branch', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: false,
- canPushToSourceBranch: true,
- conflictResolutionPath: path,
- conflictsDocsPath: '',
- },
- });
- });
-
- it('should tell you about conflicts', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).toContain(userCannotMergeText);
- });
-
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
- expect(findResolveButton().attributes('href')).toEqual(path);
- });
-
- it('should not have merge buttons', () => {
- expect(wrapper.text()).not.toContain(mergeLocallyBtnText);
- });
+ it('should tell you about conflicts', () => {
+ expect(wrapper.text()).toContain(mergeConflictsText);
+ expect(wrapper.text()).toContain(userCannotMergeText);
+ });
+
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
+ expect(findResolveButton().attributes('href')).toEqual(path);
+ });
+
+ it('should not have merge buttons', () => {
+ expect(wrapper.text()).not.toContain(mergeLocallyBtnText);
+ });
+ });
+
+ describe('when allowed to merge and push to source branch', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
});
+ });
- describe('when allowed to merge and push to source branch', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: true,
- conflictResolutionPath: path,
- conflictsDocsPath: '',
- },
- });
- });
-
- it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
- });
-
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
- expect(findResolveButton().attributes('href')).toEqual(path);
- });
-
- it('should have merge buttons', () => {
- expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
- });
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(wrapper.text()).toContain(mergeConflictsText);
+ expect(wrapper.text()).not.toContain(userCannotMergeText);
+ });
+
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
+ expect(findResolveButton().attributes('href')).toEqual(path);
+ });
+
+ it('should have merge buttons', () => {
+ expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
+ });
+ });
+
+ describe('when user does not have permission to push to source branch', () => {
+ it('should show proper message', async () => {
+ await createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: false,
+ conflictsDocsPath: '',
+ },
});
- describe('when user does not have permission to push to source branch', () => {
- it('should show proper message', async () => {
- await createComponent({
- mr: {
- canMerge: false,
- canPushToSourceBranch: false,
- conflictsDocsPath: '',
- },
- });
+ expect(wrapper.text().trim().replace(/\s\s+/g, ' ')).toContain(userCannotMergeText);
+ });
- expect(wrapper.text().trim().replace(/\s\s+/g, ' ')).toContain(userCannotMergeText);
- });
+ it('should not have action buttons', async () => {
+ await createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: false,
+ conflictsDocsPath: '',
+ },
+ });
- it('should not have action buttons', async () => {
- await createComponent({
- mr: {
- canMerge: false,
- canPushToSourceBranch: false,
- conflictsDocsPath: '',
- },
- });
-
- expect(findResolveButton().exists()).toBe(false);
- expect(findMergeLocalButton().exists()).toBe(false);
- });
-
- it('should not have resolve button when no conflict resolution path', async () => {
- await createComponent({
- mr: {
- canMerge: true,
- conflictResolutionPath: null,
- conflictsDocsPath: '',
- },
- });
+ expect(findResolveButton().exists()).toBe(false);
+ expect(findMergeLocalButton().exists()).toBe(false);
+ });
- expect(findResolveButton().exists()).toBe(false);
- });
+ it('should not have resolve button when no conflict resolution path', async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ conflictResolutionPath: null,
+ conflictsDocsPath: '',
+ },
});
- describe('when fast-forward or semi-linear merge enabled', () => {
- it('should tell you to rebase locally', async () => {
- await createComponent({
- mr: {
- shouldBeRebased: true,
- conflictsDocsPath: '',
- },
- });
+ expect(findResolveButton().exists()).toBe(false);
+ });
+ });
- expect(removeBreakLine(wrapper.text()).trim()).toContain(fastForwardMergeText);
- });
+ describe('when fast-forward or semi-linear merge enabled', () => {
+ it('should tell you to rebase locally', async () => {
+ await createComponent({
+ mr: {
+ shouldBeRebased: true,
+ conflictsDocsPath: '',
+ },
});
- describe('when source branch protected', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: true,
- conflictResolutionPath: TEST_HOST,
- sourceBranchProtected: true,
- conflictsDocsPath: '',
- },
- });
- });
+ expect(removeBreakLine(wrapper.text()).trim()).toContain(fastForwardMergeText);
+ });
+ });
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().exists()).toBe(true);
- });
+ describe('when source branch protected', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: TEST_HOST,
+ sourceBranchProtected: true,
+ conflictsDocsPath: '',
+ },
});
+ });
- describe('when source branch not protected', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: true,
- conflictResolutionPath: TEST_HOST,
- sourceBranchProtected: false,
- conflictsDocsPath: '',
- },
- });
- });
+ it('should not allow you to resolve the conflicts', () => {
+ expect(findResolveButton().exists()).toBe(false);
+ });
+ });
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
- expect(findResolveButton().attributes('href')).toEqual(TEST_HOST);
- });
+ describe('when source branch not protected', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: TEST_HOST,
+ sourceBranchProtected: false,
+ conflictsDocsPath: '',
+ },
});
});
+
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
+ expect(findResolveButton().attributes('href')).toEqual(TEST_HOST);
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
index ddce07954ab..f29cf55f7ce 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
@@ -1,26 +1,17 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import MissingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
let wrapper;
-async function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
+function factory(sourceBranchRemoved) {
wrapper = shallowMount(MissingBranchComponent, {
propsData: {
mr: { sourceBranchRemoved },
},
- provide: {
- glFeatures: { mergeRequestWidgetGraphql },
+ data() {
+ return { state: { sourceBranchExists: !sourceBranchRemoved } };
},
});
-
- if (mergeRequestWidgetGraphql) {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } });
- }
-
- await nextTick();
}
describe('MRWidgetMissingBranch', () => {
@@ -28,22 +19,16 @@ describe('MRWidgetMissingBranch', () => {
wrapper.destroy();
});
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`widget GraphQL feature flag is ${
- mergeRequestWidgetGraphql ? 'enabled' : 'disabled'
- }`, () => {
- it.each`
- sourceBranchRemoved | branchName
- ${true} | ${'source'}
- ${false} | ${'target'}
- `(
- 'should set missing branch name as $branchName when sourceBranchRemoved is $sourceBranchRemoved',
- async ({ sourceBranchRemoved, branchName }) => {
- await factory(sourceBranchRemoved, mergeRequestWidgetGraphql);
+ it.each`
+ sourceBranchRemoved | branchName
+ ${true} | ${'source'}
+ ${false} | ${'target'}
+ `(
+ 'should set missing branch name as $branchName when sourceBranchRemoved is $sourceBranchRemoved',
+ ({ sourceBranchRemoved, branchName }) => {
+ factory(sourceBranchRemoved);
- expect(wrapper.find('[data-testid="widget-content"]').text()).toContain(branchName);
- },
- );
- });
- });
+ expect(wrapper.find('[data-testid="widget-content"]').text()).toContain(branchName);
+ },
+ );
});
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 48d3f15560b..407bd60b2b7 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
@@ -60,6 +60,11 @@ const createTestMr = (customConfig) => {
translateStateToMachine: () => this.transitionStateMachine(),
state: 'open',
canMerge: true,
+ mergeable: true,
+ userPermissions: {
+ removeSourceBranch: true,
+ canMerge: true,
+ },
};
Object.assign(mr, customConfig.mr);
@@ -68,7 +73,7 @@ const createTestMr = (customConfig) => {
};
const createTestService = () => ({
- merge: jest.fn(),
+ merge: jest.fn().mockResolvedValue(),
poll: jest.fn().mockResolvedValue(),
});
@@ -87,21 +92,24 @@ const createReadyToMergeResponse = (customMr) => {
});
};
-const createComponent = (
- customConfig = {},
- mergeRequestWidgetGraphql = false,
- restructuredMrWidget = true,
-) => {
+const createComponent = (customConfig = {}, createState = true) => {
wrapper = shallowMount(ReadyToMerge, {
propsData: {
mr: createTestMr(customConfig),
service: createTestService(),
},
- provide: {
- glFeatures: {
- mergeRequestWidgetGraphql,
- restructuredMrWidget,
- },
+ data() {
+ if (createState) {
+ return {
+ loading: false,
+ state: {
+ ...createTestMr(customConfig),
+ },
+ };
+ }
+ return {
+ loading: true,
+ };
},
stubs: {
CommitEdit,
@@ -136,7 +144,7 @@ describe('ReadyToMerge', () => {
describe('computed', () => {
describe('isAutoMergeAvailable', () => {
it('should return true when at least one merge strategy is available', () => {
- createComponent();
+ createComponent({});
expect(wrapper.vm.isAutoMergeAvailable).toBe(true);
});
@@ -168,14 +176,14 @@ describe('ReadyToMerge', () => {
});
it('returns pending when pipeline is active', () => {
- createComponent({ mr: { pipeline: {}, isPipelineActive: true } });
+ createComponent({ mr: { pipeline: { active: true }, isPipelineActive: true } });
expect(wrapper.vm.status).toEqual('pending');
});
it('returns failed when pipeline is failed', () => {
createComponent({
- mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] },
+ mr: { pipeline: { status: 'FAILED' }, availableAutoMergeStrategies: [], hasCI: true },
});
expect(wrapper.vm.status).toEqual('failed');
@@ -185,7 +193,7 @@ describe('ReadyToMerge', () => {
describe('Merge Button Variant', () => {
it('defaults to confirm class', () => {
createComponent({
- mr: { availableAutoMergeStrategies: [] },
+ mr: { availableAutoMergeStrategies: [], mergeable: true },
});
expect(findMergeButton().attributes('variant')).toBe('confirm');
@@ -194,19 +202,19 @@ describe('ReadyToMerge', () => {
describe('status icon', () => {
it('defaults to tick icon', () => {
- createComponent();
+ createComponent({ mr: { mergeable: true } });
expect(wrapper.vm.iconClass).toEqual('success');
});
it('shows tick for success status', () => {
- createComponent({ mr: { pipeline: true } });
+ createComponent({ mr: { pipeline: { status: 'SUCCESS' }, mergeable: true } });
expect(wrapper.vm.iconClass).toEqual('success');
});
it('shows tick for pending status', () => {
- createComponent({ mr: { pipeline: {}, isPipelineActive: true } });
+ createComponent({ mr: { pipeline: { active: true }, mergeable: true } });
expect(wrapper.vm.iconClass).toEqual('success');
});
@@ -266,7 +274,7 @@ describe('ReadyToMerge', () => {
describe('isMergeButtonDisabled', () => {
it('should return false with initial data', () => {
- createComponent({ mr: { isMergeAllowed: true } });
+ createComponent({ mr: { isMergeAllowed: true, mergeable: false } });
expect(wrapper.vm.isMergeButtonDisabled).toBe(false);
});
@@ -283,6 +291,7 @@ describe('ReadyToMerge', () => {
isMergeAllowed: false,
availableAutoMergeStrategies: [],
onlyAllowMergeIfPipelineSucceeds: true,
+ mergeable: false,
},
});
@@ -544,7 +553,15 @@ describe('ReadyToMerge', () => {
describe('Remove source branch checkbox', () => {
describe('when user can merge but cannot delete branch', () => {
it('should be disabled in the rendered output', () => {
- createComponent();
+ createComponent({
+ mr: {
+ mergeable: true,
+ userPermissions: {
+ removeSourceBranch: false,
+ canMerge: true,
+ },
+ },
+ });
expect(wrapper.find('#remove-source-branch-input').exists()).toBe(false);
});
@@ -553,7 +570,7 @@ describe('ReadyToMerge', () => {
describe('when user can merge and can delete branch', () => {
beforeEach(() => {
createComponent({
- mr: { canRemoveSourceBranch: true },
+ mr: { canRemoveSourceBranch: true, mergeable: true },
});
});
@@ -567,7 +584,7 @@ describe('ReadyToMerge', () => {
describe('squash checkbox', () => {
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
createComponent({
- mr: { commitsCount: 2, enableSquashBeforeMerge: true },
+ mr: { commitsCount: 2, enableSquashBeforeMerge: true, mergeable: true },
});
expect(findCheckboxElement().exists()).toBe(true);
@@ -665,6 +682,7 @@ describe('ReadyToMerge', () => {
squashIsSelected: true,
enableSquashBeforeMerge: true,
commitsCount: 2,
+ mergeRequestsFfOnlyEnabled: true,
},
});
@@ -795,7 +813,9 @@ describe('ReadyToMerge', () => {
});
it('shows the diverged commits text when the source branch is behind the target', () => {
- createComponent({ mr: { divergedCommitsCount: 9001, canMerge: false } });
+ 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'),
@@ -807,7 +827,7 @@ describe('ReadyToMerge', () => {
describe('Merge button when pipeline has failed', () => {
beforeEach(() => {
createComponent({
- mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] },
+ mr: { headPipeline: { status: 'FAILED' }, availableAutoMergeStrategies: [], hasCI: true },
});
});
@@ -830,7 +850,7 @@ describe('ReadyToMerge', () => {
const USER_COMMIT_MESSAGE = 'Merge message provided manually by user';
const createDefaultGqlComponent = () =>
- createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, true);
+ createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, false);
beforeEach(() => {
readyToMergeResponseSpy = jest
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
index 7259f210b6e..82aeac1a47d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
@@ -1,101 +1,42 @@
-import Vue, { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
+import { mount } from '@vue/test-utils';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
-import toast from '~/vue_shared/plugins/global_toast';
-import eventHub from '~/vue_merge_request_widget/event_hub';
-jest.mock('~/vue_shared/plugins/global_toast');
-
-const createComponent = () => {
- const Component = Vue.extend(WorkInProgress);
- const mr = {
- title: 'The best MR ever',
- removeWIPPath: '/path/to/remove/wip',
- };
- const service = {
- removeWIP() {},
- };
- return new Component({
- el: document.createElement('div'),
- propsData: { mr, service },
+let wrapper;
+
+const createComponent = (updateMergeRequest = true) => {
+ wrapper = mount(WorkInProgress, {
+ propsData: {
+ mr: {},
+ },
+ data() {
+ return {
+ userPermissions: {
+ updateMergeRequest,
+ },
+ };
+ },
});
};
-describe('Wip', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr, service } = WorkInProgress.props;
-
- expect(mr.type instanceof Object).toBe(true);
- expect(mr.required).toBe(true);
-
- expect(service.type instanceof Object).toBe(true);
- expect(service.required).toBe(true);
- });
- });
-
- describe('data', () => {
- it('should have default data', () => {
- const vm = createComponent();
-
- expect(vm.isMakingRequest).toBe(false);
- });
- });
-
- describe('methods', () => {
- const mrObj = {
- is_new_mr_data: true,
- };
-
- describe('handleRemoveDraft', () => {
- it('should make a request to service and handle response', async () => {
- const vm = createComponent();
-
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(vm.service, 'removeWIP').mockReturnValue(
- new Promise((resolve) => {
- resolve({
- data: mrObj,
- });
- }),
- );
-
- vm.handleRemoveDraft();
-
- await waitForPromises();
-
- expect(vm.isMakingRequest).toBe(true);
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.');
- });
- });
+describe('Merge request widget draft state component', () => {
+ afterEach(() => {
+ wrapper.destroy();
});
describe('template', () => {
- let vm;
- let el;
-
- beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
- });
-
it('should have correct elements', () => {
- expect(el.classList.contains('mr-widget-body')).toBe(true);
- expect(el.innerText).toContain(
+ createComponent(true);
+
+ expect(wrapper.text()).toContain(
"Merge blocked: merge request must be marked as ready. It's still marked as draft.",
);
- expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain(
- 'Mark as ready',
- );
+ expect(wrapper.find('[data-testid="removeWipButton"]').text()).toContain('Mark as ready');
});
- it('should not show removeWIP button is user cannot update MR', async () => {
- vm.mr.removeWIPPath = '';
-
- await nextTick();
+ it('should not show removeWIP button is user cannot update MR', () => {
+ createComponent(false);
- expect(el.querySelector('.js-remove-draft')).toBeNull();
+ expect(wrapper.find('[data-testid="removeWipButton"]').exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
index 08424077269..e9a34453930 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue renders given data 1`] = `
-"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\">
+"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\" helppopover=\\"[object Object]\\" actionbuttons=\\"\\">
<div class=\\"gl-display-flex gl-flex-direction-column\\">
<div>
<p class=\\"gl-mb-0\\">Main text for the row</p>
<gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
<!---->
- <gl-badge-stub size=\\"md\\" variant=\\"info\\">
+ <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
Badge is optional. Text to be displayed inside badge
</gl-badge-stub>
<actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
@@ -15,7 +15,7 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
</div>
<ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\">
<li>
- <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" data-qa-selector=\\"child_content\\">
+ <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" actionbuttons=\\"\\" data-qa-selector=\\"child_content\\">
<div class=\\"gl-display-flex gl-flex-direction-column\\">
<div>
<p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
index b7753a58747..527e800ddcf 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -25,6 +25,10 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
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',
+ helpPopover: {
+ options: { title: 'Widget help popover title' },
+ content: { text: 'Widget help popover content' },
+ },
icon: {
name: EXTENSION_ICONS.success,
},
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 9eddd091ad0..e4bee6b8652 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,11 +1,15 @@
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 HelpPopover from '~/vue_shared/components/help_popover.vue';
describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', () => {
let wrapper;
const findStatusIcon = () => wrapper.findComponent(StatusIcon);
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
+ const findActionButtons = () => wrapper.findComponent(ActionButtons);
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(WidgetContentRow, {
@@ -61,5 +65,38 @@ describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue',
createComponent({ propsData: { header: '<b role="header">this is a header</b>' } });
expect(wrapper.findByText('<b role="header">this is a header</b>').exists()).toBe(true);
});
+
+ it('renders a help popover', () => {
+ createComponent({
+ propsData: {
+ helpPopover: {
+ options: { title: 'Help popover title' },
+ content: { text: 'Help popover content', learnMorePath: '/path/to/docs' },
+ },
+ },
+ });
+
+ expect(findHelpPopover().props('options')).toEqual({ title: 'Help popover title' });
+ 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');
+ });
+
+ it('does not render help popover when it is not provided', () => {
+ createComponent({});
+ expect(findHelpPopover().exists()).toBe(false);
+ });
+
+ it('does not display action buttons if actionButtons is not provided', () => {
+ createComponent({});
+ expect(findActionButtons().exists()).toBe(false);
+ });
+
+ it('does display action buttons if actionButtons is provided', () => {
+ const actionButtons = [{ text: 'click-me', href: '#' }];
+
+ createComponent({ propsData: { actionButtons } });
+ expect(findActionButtons().props('tertiaryButtons')).toEqual(actionButtons);
+ });
});
});
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 4826fecf98d..9635e050e4d 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
@@ -1,12 +1,21 @@
import { nextTick } from 'vue';
import * as Sentry from '@sentry/browser';
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 Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+jest.mock('~/vue_merge_request_widget/components/extensions/telemetry', () => ({
+ createTelemetryHub: jest.fn().mockReturnValue({
+ viewed: jest.fn(),
+ expanded: jest.fn(),
+ fullReportClicked: jest.fn(),
+ }),
+}));
+
describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
let wrapper;
@@ -14,13 +23,15 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const findExpandedSection = () => wrapper.findByTestId('widget-extension-collapsed-section');
const findActionButtons = () => wrapper.findComponent(ActionButtons);
const findToggleButton = () => wrapper.findByTestId('toggle-button');
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(Widget, {
propsData: {
isCollapsible: false,
loadingText: 'Loading widget',
- widgetName: 'MyWidget',
+ widgetName: 'WidgetTest',
+ fetchCollapsedData: () => Promise.resolve([]),
value: {
collapsed: null,
expanded: null,
@@ -30,6 +41,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
slots,
stubs: {
StatusIcon,
+ ActionButtons,
ContentRow: WidgetContentRow,
},
});
@@ -52,8 +64,9 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
it('sets the error text when fetch method fails', async () => {
- const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
- createComponent({ propsData: { fetchCollapsedData } });
+ createComponent({
+ propsData: { fetchCollapsedData: jest.fn().mockRejectedValue('Something went wrong') },
+ });
await waitForPromises();
expect(wrapper.findByText('Failed to load').exists()).toBe(true);
expect(findStatusIcon().props()).toMatchObject({ iconName: 'failed', isLoading: false });
@@ -79,12 +92,24 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
it('displays the loading text', async () => {
- const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
- createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
+ createComponent({
+ propsData: {
+ statusIconName: 'warning',
+ },
+ });
+
expect(wrapper.text()).not.toContain('Loading');
await nextTick();
expect(wrapper.text()).toContain('Loading');
});
+
+ it('validates widget name', () => {
+ expect(() => {
+ createComponent({
+ propsData: { widgetName: 'InvalidWidgetName' },
+ });
+ }).toThrow();
+ });
});
describe('fetch', () => {
@@ -136,7 +161,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
summary: 'Hello world',
- fetchCollapsedData: () => Promise.resolve(),
},
});
@@ -145,28 +169,22 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it.todo('displays content property when content slot is not provided');
- it('displays the summary slot when provided', () => {
+ it('displays the summary slot when provided', async () => {
createComponent({
- propsData: {
- fetchCollapsedData: () => Promise.resolve(),
- },
slots: {
summary: '<b>More complex summary</b>',
},
});
+ await waitForPromises();
+
expect(wrapper.findByTestId('widget-extension-top-level-summary').text()).toBe(
'More complex summary',
);
});
it('does not display action buttons if actionButtons is not provided', () => {
- createComponent({
- propsData: {
- fetchCollapsedData: () => Promise.resolve(),
- },
- });
-
+ createComponent();
expect(findActionButtons().exists()).toBe(false);
});
@@ -175,7 +193,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
- fetchCollapsedData: () => Promise.resolve(),
actionButtons,
},
});
@@ -184,12 +201,34 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
});
+ describe('help popover', () => {
+ it('renders a help popover', () => {
+ createComponent({
+ propsData: {
+ helpPopover: {
+ options: { title: 'My help popover title' },
+ content: { text: 'Help popover content', learnMorePath: '/path/to/docs' },
+ },
+ },
+ });
+
+ expect(findHelpPopover().props('options')).toEqual({ title: 'My help popover title' });
+ 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');
+ });
+
+ it('does not render help popover when it is not provided', () => {
+ createComponent();
+ expect(findHelpPopover().exists()).toBe(false);
+ });
+ });
+
describe('handle collapse toggle', () => {
it('displays the toggle button correctly', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve(),
},
slots: {
content: '<b>More complex content</b>',
@@ -204,7 +243,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve(),
},
slots: {
content: '<b>More complex content</b>',
@@ -221,7 +259,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: false,
- fetchCollapsedData: () => Promise.resolve(),
},
});
@@ -278,7 +315,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve([]),
fetchExpandedData,
},
});
@@ -306,7 +342,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve([]),
fetchExpandedData,
},
});
@@ -323,4 +358,54 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(wrapper.findByText('Failed to load').exists()).toBe(false);
});
});
+
+ describe('telemetry - enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ actionButtons: [
+ {
+ fullReport: true,
+ href: '#',
+ target: '_blank',
+ id: 'full-report-button',
+ text: 'Full Report',
+ },
+ ],
+ },
+ });
+ });
+
+ it('should call create a telemetry hub', () => {
+ expect(wrapper.vm.telemetryHub).not.toBe(null);
+ });
+
+ it('should call the viewed state', async () => {
+ await nextTick();
+ expect(wrapper.vm.telemetryHub.viewed).toHaveBeenCalledTimes(1);
+ });
+
+ 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');
+ await nextTick();
+ expect(wrapper.vm.telemetryHub.fullReportClicked).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('telemetry - disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ telemetry: false,
+ },
+ });
+ });
+
+ it('should not call create a telemetry hub', () => {
+ expect(wrapper.vm.telemetryHub).toBe(null);
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
index 58dadb2c679..41df485b0de 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
@@ -23,11 +23,7 @@ import {
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('DeploymentAction component', () => {
let wrapper;
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js
new file mode 100644
index 00000000000..c6b73f63301
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js
@@ -0,0 +1,42 @@
+import { mount } from '@vue/test-utils';
+import { GlTruncate, GlLink } from '@gitlab/ui';
+import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
+import { deploymentMockData } from './deployment_mock_data';
+
+// This component is well covered in ./deployment_spec.js
+// more component-specific tests are added below
+describe('Deployment Info component', () => {
+ let wrapper;
+
+ const defaultDeploymentInfoOptions = {
+ computedDeploymentStatus: 'computed deployment status',
+ deployment: deploymentMockData,
+ showMetrics: false,
+ };
+
+ const factory = (options = {}) => {
+ const componentProps = { ...defaultDeploymentInfoOptions, ...options };
+ const componentOptions = { propsData: componentProps };
+ wrapper = mount(DeploymentInfo, componentOptions);
+ };
+
+ beforeEach(() => {
+ factory();
+ });
+
+ it('should render gl-truncate for environment name', () => {
+ const envNameComponent = wrapper.findComponent(GlTruncate);
+ expect(envNameComponent.exists()).toBe(true, 'We should use gl-truncate for environment name');
+ expect(envNameComponent.props()).toEqual({
+ text: deploymentMockData.name,
+ withTooltip: true,
+ position: 'middle',
+ });
+ });
+
+ it('should have a link with a correct href to deployed environment', () => {
+ const envLink = wrapper.findComponent(GlLink);
+ expect(envLink.exists()).toBe(true, 'We should have gl-link pointing to deployed environment');
+ expect(envLink.attributes().href).toBe(deploymentMockData.url);
+ });
+});
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 6622749da92..0f4637d18d9 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
@@ -4,6 +4,8 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
+import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
+import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
@@ -22,6 +24,10 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.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';
+import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
+import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql';
+import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import mockData from './mock_data';
import {
@@ -83,7 +89,39 @@ describe('MrWidgetOptions', () => {
propsData: {
mrData: { ...mrData },
},
+ data() {
+ return { loading: false };
+ },
+
...options,
+ apolloProvider: createMockApollo([
+ [
+ getStateQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ ...getStateQueryResponse.data.project,
+ mergeRequest: {
+ ...getStateQueryResponse.data.project.mergeRequest,
+ mergeError: mrData.mergeError || null,
+ },
+ },
+ },
+ }),
+ ],
+ [readyToMergeQuery, jest.fn().mockResolvedValue(readyToMergeResponse)],
+ [
+ userPermissionsQuery,
+ jest.fn().mockResolvedValue({
+ data: { project: { mergeRequest: { userPermissions: {} } } },
+ }),
+ ],
+ [
+ conflictsStateQuery,
+ jest.fn().mockResolvedValue({ data: { project: { mergeRequest: {} } } }),
+ ],
+ ...(options.apolloMock || []),
+ ]),
});
return axios.waitForAll();
@@ -563,21 +601,6 @@ describe('MrWidgetOptions', () => {
});
});
- describe('code quality widget', () => {
- beforeEach(() => {
- jest.spyOn(document, 'dispatchEvent');
- });
- it('renders the component when refactorCodeQualityExtension is false', () => {
- createComponent(mockData, {}, { refactorCodeQualityExtension: false });
- expect(wrapper.find('.js-codequality-widget').exists()).toBe(true);
- });
-
- it('does not render the component when refactorCodeQualityExtension is true', () => {
- createComponent(mockData, {}, { refactorCodeQualityExtension: true });
- expect(wrapper.find('.js-codequality-widget').exists()).toBe(true);
- });
- });
-
describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => {
beforeEach(() => {
@@ -784,12 +807,12 @@ describe('MrWidgetOptions', () => {
mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
return createComponent(mrData, {
- apolloProvider: createMockApollo([
+ apolloMock: [
[
securityReportMergeRequestDownloadPathsQuery,
async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
],
- ]),
+ ],
});
};
@@ -852,8 +875,10 @@ describe('MrWidgetOptions', () => {
${'closed'} | ${false} | ${'hides'}
${'merged'} | ${true} | ${'shows'}
${'open'} | ${true} | ${'shows'}
- `('$showText merge error when state is $state', ({ state, show }) => {
- createComponent({ ...mockData, state, merge_error: 'Error!' });
+ `('$showText merge error when state is $state', async ({ state, show }) => {
+ createComponent({ ...mockData, state, mergeError: 'Error!' });
+
+ await waitForPromises();
expect(wrapper.find('[data-testid="merge_error"]').exists()).toBe(show);
});
@@ -917,8 +942,7 @@ describe('MrWidgetOptions', () => {
});
it('extension polling is not called if enablePolling flag is not passed', () => {
- // called one time due to parent component polling (mount)
- expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollRequest).toHaveBeenCalledTimes(0);
});
});
@@ -1004,7 +1028,7 @@ describe('MrWidgetOptions', () => {
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollRequest).toHaveBeenCalledTimes(1);
});
});
@@ -1042,7 +1066,7 @@ describe('MrWidgetOptions', () => {
registerExtension(pollingErrorExtension);
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollRequest).toHaveBeenCalledTimes(1);
});
it('captures sentry error and displays error when poll has failed', async () => {
@@ -1085,7 +1109,7 @@ describe('MrWidgetOptions', () => {
await nextTick();
await waitForPromises();
- expect(Sentry.captureException).toHaveBeenCalledTimes(1);
+ expect(Sentry.captureException).toHaveBeenCalledTimes(2);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error'));
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
});
diff --git a/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
index 3cdb4265ef0..37df041210c 100644
--- a/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
@@ -21,22 +21,9 @@ describe('MergeRequestStore', () => {
});
describe('setData', () => {
- it('should set isSHAMismatch when the diff SHA changes', () => {
- store.setData({ ...mockData, diff_head_sha: 'a-different-string' });
-
- expect(store.isSHAMismatch).toBe(true);
- });
-
- it('should not set isSHAMismatch when other data changes', () => {
- store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
-
- expect(store.isSHAMismatch).toBe(false);
- });
-
it('should update cached sha after rebasing', () => {
store.setData({ ...mockData, diff_head_sha: 'abc123' }, true);
- expect(store.isSHAMismatch).toBe(false);
expect(store.sha).toBe('abc123');
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index d309432bc63..3bc191d988f 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -30,7 +30,7 @@ describe('AlertDetails', () => {
const projectPath = 'root/alerts';
const projectIssuesPath = 'root/alerts/-/issues';
const projectId = '1';
- const $router = { replace: jest.fn() };
+ const $router = { push: jest.fn() };
function mountComponent({
data,
@@ -352,7 +352,7 @@ describe('AlertDetails', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentTabIndex: index });
- expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
+ expect($router.push).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
});
});
});
diff --git a/spec/frontend/vue_shared/alert_details/router_spec.js b/spec/frontend/vue_shared/alert_details/router_spec.js
new file mode 100644
index 00000000000..e3efc104862
--- /dev/null
+++ b/spec/frontend/vue_shared/alert_details/router_spec.js
@@ -0,0 +1,35 @@
+import createRouter from '~/vue_shared/alert_details/router';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const BASE_PATH = '/-/alert_management/1/details';
+const EMPTY_HASH = '';
+const NOOP = () => {};
+
+describe('AlertDetails router', () => {
+ const originalLocation = window.location.href;
+ let router;
+
+ beforeEach(() => {
+ setWindowLocation(originalLocation);
+ router = createRouter(BASE_PATH);
+ });
+
+ describe('redirects hash route mode URLs to history route mode', () => {
+ it.each`
+ hashPath | historyPath
+ ${'/#/overview'} | ${'/overview'}
+ ${'#/overview'} | ${'/overview'}
+ ${'/#/'} | ${'/'}
+ ${'#/'} | ${'/'}
+ ${'/#'} | ${'/'}
+ ${'#'} | ${'/'}
+ ${'/'} | ${'/'}
+ ${'/overview'} | ${'/overview'}
+ `('should redirect "$hashPath" to "$historyPath"', ({ hashPath, historyPath }) => {
+ router.push(hashPath, NOOP);
+
+ expect(window.location.hash).toBe(EMPTY_HASH);
+ expect(window.location.pathname).toBe(BASE_PATH + historyPath);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index f5a545891d5..c3a71d7fda3 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -106,7 +106,7 @@ describe('File row component', () => {
level: 2,
});
- expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('32px');
+ expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('16px');
});
it('renders header for file', () => {
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
deleted file mode 100644
index 38f28837cc1..00000000000
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { mockTracking } from 'helpers/tracking_helper';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import axios from '~/lib/utils/axios_utils';
-import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
-
-describe('GitlabVersionCheck', () => {
- let wrapper;
- let mock;
-
- const UPGRADE_DOCS_URL = helpPagePath('update/index');
-
- const defaultResponse = {
- code: 200,
- res: { severity: 'success' },
- };
-
- const createComponent = (mockResponse) => {
- const response = {
- ...defaultResponse,
- ...mockResponse,
- };
-
- mock = new MockAdapter(axios);
- mock.onGet().replyOnce(response.code, response.res);
-
- wrapper = shallowMountExtended(GitlabVersionCheck);
- };
-
- const dummyGon = {
- relative_url_root: '/',
- };
-
- let originalGon;
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- window.gon = originalGon;
- });
-
- const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
- const findGlBadge = () => wrapper.findComponent(GlBadge);
-
- describe.each`
- root | description
- ${'/'} | ${'not used (uses its own (sub)domain)'}
- ${'/gitlab'} | ${'custom path'}
- ${'/service/gitlab'} | ${'custom path with 2 depth'}
- `('path for version_check.json', ({ root, description }) => {
- describe(`when relative url is ${description}: ${root}`, () => {
- beforeEach(async () => {
- originalGon = window.gon;
- window.gon = { ...dummyGon };
- window.gon.relative_url_root = root;
- createComponent(defaultResponse);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it('reflects the relative url setting', () => {
- expect(mock.history.get.length).toBe(1);
-
- const pathRegex = new RegExp(`^${root}`);
- expect(mock.history.get[0].url).toMatch(pathRegex);
- });
- });
- });
-
- describe('template', () => {
- describe.each`
- description | mockResponse | renders
- ${'successful but null'} | ${{ code: 200, res: null }} | ${false}
- ${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true}
- ${'an error'} | ${{ code: 500, res: null }} | ${false}
- `('version_check.json response', ({ description, mockResponse, renders }) => {
- describe(`is ${description}`, () => {
- beforeEach(async () => {
- createComponent(mockResponse);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it(`does${renders ? '' : ' not'} render Badge Click Wrapper and GlBadge`, () => {
- expect(findGlBadgeClickWrapper().exists()).toBe(renders);
- expect(findGlBadge().exists()).toBe(renders);
- });
- });
- });
-
- describe.each`
- mockResponse | expectedUI
- ${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }}
- ${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }}
- ${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
- `('badge ui', ({ mockResponse, expectedUI }) => {
- describe(`when response is ${mockResponse.res.severity}`, () => {
- let trackingSpy;
-
- beforeEach(async () => {
- createComponent(mockResponse);
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it(`title is ${expectedUI.title}`, () => {
- expect(findGlBadge().text()).toBe(expectedUI.title);
- });
-
- it(`variant is ${expectedUI.variant}`, () => {
- expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
- });
-
- it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'rendered_version_badge', {
- label: expectedUI.title,
- });
- });
-
- it(`link is ${UPGRADE_DOCS_URL}`, () => {
- expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
- });
-
- it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
- await findGlBadgeClickWrapper().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_version_badge', {
- label: expectedUI.title,
- });
- });
- });
- });
- });
-});
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
new file mode 100644
index 00000000000..f959d2225fa
--- /dev/null
+++ b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
@@ -0,0 +1,202 @@
+import { nextTick } from 'vue';
+import { GlListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import { createAlert } from '~/flash';
+import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
+import {
+ TOGGLE_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+ QUERY_TOO_SHORT_MESSAGE,
+} from '~/vue_shared/components/group_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/flash');
+
+describe('GroupSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Mocks
+ const groupMock = {
+ full_name: 'selectedGroup',
+ id: '1',
+ };
+ const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
+
+ // Props
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlListbox);
+ const findInput = () => wrapper.findByTestId('input');
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupSelect, {
+ propsData: {
+ inputName,
+ inputId,
+ ...props,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+ const search = (searchString) => findListbox().vm.$emit('search', searchString);
+ const createComponentWithGroups = () => {
+ mock.onGet('/api/undefined/groups.json').reply(200, [groupMock]);
+ createComponent();
+ openListbox();
+ return waitForPromises();
+ };
+ const selectGroup = () => {
+ findListbox().vm.$emit('select', groupMock.id);
+ return nextTick();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on mount', () => {
+ it('fetches groups when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ });
+
+ describe('with an initial selection', () => {
+ it('if the selected group is not part of the fetched list, fetches it individually', async () => {
+ mock.onGet(groupEndpoint).reply(200, groupMock);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it('show an error if fetching the individual group fails', async () => {
+ mock
+ .onGet('/api/undefined/groups.json')
+ .reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
+ mock.onGet(groupEndpoint).reply(500);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FETCH_GROUP_ERROR,
+ error: expect.any(Error),
+ parent: wrapper.vm.$el,
+ });
+ });
+ });
+ });
+
+ it('shows an error when fetching groups fails', async () => {
+ mock.onGet('/api/undefined/groups.json').reply(500);
+ createComponent();
+ openListbox();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FETCH_GROUPS_ERROR,
+ error: expect.any(Error),
+ parent: wrapper.vm.$el,
+ });
+ });
+
+ describe('selection', () => {
+ it('uses the default toggle text while no group is selected', async () => {
+ await createComponentWithGroups();
+
+ expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
+ });
+
+ describe('once a group is selected', () => {
+ it(`uses the selected group's name as the toggle text`, async () => {
+ await createComponentWithGroups();
+ await selectGroup();
+
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it(`uses the selected group's ID as the listbox' and input value`, async () => {
+ await createComponentWithGroups();
+ await selectGroup();
+
+ expect(findListbox().attributes('selected')).toBe(groupMock.id);
+ expect(findInput().attributes('value')).toBe(groupMock.id);
+ });
+
+ it(`on reset, falls back to the default toggle text`, async () => {
+ await createComponentWithGroups();
+ await selectGroup();
+
+ findListbox().vm.$emit('reset');
+ await nextTick();
+
+ expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
+ });
+ });
+ });
+
+ describe('search', () => {
+ it('sets `searching` to `true` when first opening the dropdown', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ openListbox();
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('sets `searching` to `true` while searching', async () => {
+ await createComponentWithGroups();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ search('foo');
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('fetches groups matching the search string', async () => {
+ const searchString = 'searchString';
+ await createComponentWithGroups();
+
+ expect(mock.history.get).toHaveLength(1);
+
+ search(searchString);
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(2);
+ expect(mock.history.get[1].params).toStrictEqual({ search: searchString });
+ });
+
+ it('shows a notice if the search query is too short', async () => {
+ const searchString = 'a';
+ await createComponentWithGroups();
+ search(searchString);
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 6fd5ae0e946..77c03dc0c3c 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -96,6 +96,20 @@ describe('HelpPopover', () => {
});
});
+ describe('with alternative icon', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ icon: 'information-o',
+ },
+ });
+ });
+
+ it('uses the given icon', () => {
+ expect(findQuestionButton().props('icon')).toBe('information-o');
+ });
+ });
+
describe('with custom slots', () => {
const titleSlot = '<h1>title</h1>';
const defaultSlot = '<strong>content</strong>';
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 f7e93f45148..625e67c7cc1 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -27,7 +27,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const formFieldAriaLabel = 'Edit your content';
let mock;
- const buildWrapper = ({ propsData = {}, attachTo } = {}) => {
+ const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
wrapper = mountExtended(MarkdownEditor, {
attachTo,
propsData: {
@@ -45,6 +45,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
},
stubs: {
BubbleMenu: stubComponent(BubbleMenu),
+ ...stubs,
},
});
};
@@ -138,9 +139,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('input')).toEqual([[newValue]]);
});
- describe('when initOnAutofocus is true', () => {
+ describe('when autofocus is true', () => {
beforeEach(async () => {
- buildWrapper({ attachTo: document.body, propsData: { initOnAutofocus: true } });
+ buildWrapper({ attachTo: document.body, propsData: { autofocus: true } });
await nextTick();
});
@@ -171,7 +172,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
markdown: value,
- autofocus: 'end',
}),
);
});
@@ -204,10 +204,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
});
- describe('when initOnAutofocus is true', () => {
+ describe('when autofocus is true', () => {
beforeEach(() => {
- buildWrapper({ propsData: { initOnAutofocus: true } });
- findLocalStorageSync().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ buildWrapper({
+ propsData: { autofocus: true },
+ stubs: { ContentEditor: stubComponent(ContentEditor) },
+ });
});
it('sets the content editor autofocus property to end', () => {
@@ -247,19 +249,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('updates localStorage value', () => {
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
});
-
- it('sets the textarea as the activeElement in the document', async () => {
- // The component should be rebuilt to attach it to the document body
- buildWrapper({ attachTo: document.body });
- await findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
-
- expect(findContentEditor().exists()).toBe(true);
-
- await findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
- await findSegmentedControl().vm.$emit('change', EDITING_MODE_MARKDOWN_FIELD);
-
- expect(document.activeElement).toBe(findTextarea().element);
- });
});
describe('when content editor emits loading event', () => {
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
new file mode 100644
index 00000000000..8edcb905096
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -0,0 +1,205 @@
+import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import { 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';
+import { contentTop } from '~/lib/utils/common_utils';
+
+jest.mock('~/vue_shared/components/markdown_drawer/utils/fetch', () => ({
+ getRenderedMarkdown: jest.fn().mockReturnValue({
+ title: 'test title test',
+ body: `<div id="content-body">
+ <div class="documentation md gl-mt-3">
+ test body
+ </div>
+ </div>`,
+ }),
+}));
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ contentTop: jest.fn(),
+}));
+
+describe('MarkdownDrawer', () => {
+ let wrapper;
+ const defaultProps = {
+ documentPath: 'user/search/global_search/advanced_search_syntax.json',
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(MarkdownDrawer, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ Object.keys(cache).forEach((key) => delete cache[key]);
+ });
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
+ const findDrawerTitle = () => wrapper.findComponent('[data-testid="title-element"]');
+ const findDrawerBody = () => wrapper.findComponent({ ref: 'content-element' });
+
+ describe('component', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders correctly', () => {
+ expect(findDrawer().exists()).toBe(true);
+ expect(findDrawerTitle().text()).toBe('test title test');
+ expect(findDrawerBody().text()).toBe('test body');
+ });
+ });
+
+ describe.each`
+ hasNavbar | navbarHeight
+ ${false} | ${0}
+ ${true} | ${100}
+ `('computes offsetTop', ({ hasNavbar, navbarHeight }) => {
+ beforeEach(() => {
+ global.document.querySelector = jest.fn(() =>
+ hasNavbar
+ ? {
+ dataset: {
+ page: 'test',
+ },
+ }
+ : undefined,
+ );
+ contentTop.mockReturnValue(navbarHeight);
+ createComponent();
+ });
+
+ afterEach(() => {
+ contentTop.mockClear();
+ });
+
+ it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, () => {
+ expect(findDrawer().attributes('headerheight')).toBe(`${navbarHeight}px`);
+ });
+ });
+
+ describe('watcher', () => {
+ let renderGLFMSpy;
+ let fetchMarkdownSpy;
+
+ beforeEach(async () => {
+ 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',
+ },
+ }));
+ createComponent();
+ await nextTick();
+ });
+
+ afterEach(() => {
+ renderGLFMSpy.mockClear();
+ fetchMarkdownSpy.mockClear();
+ });
+
+ it('for documentPath triggers fetch', async () => {
+ expect(fetchMarkdownSpy).toHaveBeenCalledTimes(1);
+
+ await wrapper.setProps({ documentPath: '/test/me' });
+ await nextTick();
+
+ expect(fetchMarkdownSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('for open triggers renderGLFM', async () => {
+ wrapper.vm.fetchMarkdown();
+ wrapper.vm.openDrawer();
+ await nextTick();
+ expect(renderGLFMSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('Markdown fetching', () => {
+ let renderGLFMSpy;
+
+ beforeEach(async () => {
+ renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
+ createComponent();
+ await nextTick();
+ });
+
+ afterEach(() => {
+ renderGLFMSpy.mockClear();
+ });
+
+ it('fetches the Markdown and caches it', async () => {
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
+ expect(Object.keys(cache)).toHaveLength(1);
+ });
+
+ it('when the document changes, fetches it and caches it as well', async () => {
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
+ expect(Object.keys(cache)).toHaveLength(1);
+
+ await wrapper.setProps({ documentPath: '/test/me2' });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+ });
+
+ it('when re-using an already fetched document, gets it from the cache', async () => {
+ await wrapper.setProps({ documentPath: '/test/me2' });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+
+ await wrapper.setProps({ documentPath: defaultProps.documentPath });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+ });
+ });
+
+ describe('Markdown fetching returns error', () => {
+ beforeEach(async () => {
+ getRenderedMarkdown.mockReturnValue({
+ hasFetchError: true,
+ });
+
+ createComponent();
+ await nextTick();
+ });
+ afterEach(() => {
+ getRenderedMarkdown.mockClear();
+ });
+ it('shows alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('While Markdown is fetching', () => {
+ beforeEach(async () => {
+ getRenderedMarkdown.mockReturnValue(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ getRenderedMarkdown.mockClear();
+ });
+
+ it('shows skeleton', async () => {
+ expect(findSkeleton().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js b/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js
new file mode 100644
index 00000000000..53b40407556
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js
@@ -0,0 +1,42 @@
+export const MOCK_HTML = `<!DOCTYPE html>
+<html>
+<body>
+ <div id="content-body">
+ <h1>test title <strong>test</strong></h1>
+ <div class="documentation md gl-mt-3">
+ <a href="../advanced_search.md">Advanced Search</a>
+ <a href="../advanced_search2.md">Advanced Search2</a>
+ <h2>test header h2</h2>
+ <table class="testClass">
+ <tr>
+ <td>Emil</td>
+ <td>Tobias</td>
+ <td>Linus</td>
+ </tr>
+ <tr>
+ <td>16</td>
+ <td>14</td>
+ <td>10</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+</body>
+</html>`.replace(/\n/g, '');
+
+export const MOCK_DRAWER_DATA = {
+ hasFetchError: false,
+ title: 'test title test',
+ body: ` <div id="content-body"> <div class="documentation md gl-mt-3"> <a href="../advanced_search.md">Advanced Search</a> <a href="../advanced_search2.md">Advanced Search2</a> <h2>test header h2</h2> <table class="testClass"> <tbody><tr> <td>Emil</td> <td>Tobias</td> <td>Linus</td> </tr> <tr> <td>16</td> <td>14</td> <td>10</td> </tr> </tbody></table> </div> </div>`,
+};
+
+export const MOCK_DRAWER_DATA_ERROR = {
+ hasFetchError: true,
+};
+
+export const MOCK_TABLE_DATA_BEFORE = `<head></head><body><h1>test</h1></test><table><tbody><tr><td></td></tr></tbody></table></body>`;
+
+export const MOCK_HTML_DATA_AFTER = {
+ body: '<table><tbody><tr><td></td></tr></tbody></table>',
+ title: 'test',
+};
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
new file mode 100644
index 00000000000..ff07b2cf838
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
@@ -0,0 +1,43 @@
+import MockAdapter from 'axios-mock-adapter';
+import {
+ getRenderedMarkdown,
+ splitDocument,
+} from '~/vue_shared/components/markdown_drawer/utils/fetch';
+import axios from '~/lib/utils/axios_utils';
+import {
+ MOCK_HTML,
+ MOCK_DRAWER_DATA,
+ MOCK_DRAWER_DATA_ERROR,
+ MOCK_TABLE_DATA_BEFORE,
+ MOCK_HTML_DATA_AFTER,
+} from '../mock_data';
+
+describe('utils/fetch', () => {
+ let mock;
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe.each`
+ axiosMock | type | toExpect
+ ${{ code: 200, res: { html: 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(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet().reply(axiosMock.code, axiosMock.res);
+ });
+ it(`should update drawer correctly`, async () => {
+ expect(await getRenderedMarkdown('/any/path')).toStrictEqual(toExpect);
+ });
+ });
+ });
+
+ describe('splitDocument', () => {
+ it(`should update tables correctly`, () => {
+ expect(splitDocument(MOCK_TABLE_DATA_BEFORE)).toStrictEqual(MOCK_HTML_DATA_AFTER);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/namespace_select/mock_data.js b/spec/frontend/vue_shared/components/namespace_select/mock_data.js
deleted file mode 100644
index cfd521c67cb..00000000000
--- a/spec/frontend/vue_shared/components/namespace_select/mock_data.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export const groupNamespaces = [
- { id: 1, name: 'Group 1', humanName: 'Group 1' },
- { id: 2, name: 'Subgroup 1', humanName: 'Group 1 / Subgroup 1' },
-];
-
-export const userNamespaces = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
deleted file mode 100644
index d930ef63dad..00000000000
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import { nextTick } from 'vue';
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlIntersectionObserver,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import NamespaceSelect, {
- i18n,
- EMPTY_NAMESPACE_ID,
-} from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
-import { userNamespaces, groupNamespaces } from './mock_data';
-
-const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
-const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
-const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
-
-describe('NamespaceSelectDeprecated', () => {
- let wrapper;
-
- const createComponent = (props = {}) =>
- shallowMountExtended(NamespaceSelect, {
- propsData: {
- userNamespaces,
- groupNamespaces,
- ...props,
- },
- stubs: {
- // We have to "full" mount GlDropdown so that slot children will render
- GlDropdown,
- },
- });
-
- const wrappersText = (arr) => arr.wrappers.map((w) => w.text());
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownText = () => findDropdown().props('text');
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findGroupDropdownItems = () =>
- wrapper.findByTestId('namespace-list-groups').findAllComponents(GlDropdownItem);
- const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
- const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const search = (term) => findSearchBox().vm.$emit('input', term);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('default', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- it('renders the dropdown', () => {
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('renders each dropdown item', () => {
- expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName));
- });
-
- it('renders default dropdown text', () => {
- expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT);
- });
-
- it('splits group and user namespaces', () => {
- const headers = findSectionHeaders();
- expect(wrappersText(headers)).toEqual([i18n.USERS, i18n.GROUPS]);
- });
-
- it('does not render wrapper as full width', () => {
- expect(findDropdown().attributes('block')).toBeUndefined();
- });
- });
-
- it('with defaultText, it overrides dropdown text', () => {
- const textOverride = 'Select an option';
-
- wrapper = createComponent({ defaultText: textOverride });
-
- expect(findDropdownText()).toBe(textOverride);
- });
-
- it('with includeHeaders=false, hides group/user headers', () => {
- wrapper = createComponent({ includeHeaders: false });
-
- expect(findSectionHeaders()).toHaveLength(0);
- });
-
- it('with fullWidth=true, sets the dropdown to full width', () => {
- wrapper = createComponent({ fullWidth: true });
-
- expect(findDropdown().attributes('block')).toBe('true');
- });
-
- describe('with search', () => {
- it.each`
- term | includeEmptyNamespace | shouldFilterNamespaces | expectedItems
- ${''} | ${false} | ${true} | ${[...userNamespaces, ...groupNamespaces]}
- ${'sub'} | ${false} | ${true} | ${[groupNamespaces[1]]}
- ${'User'} | ${false} | ${true} | ${[...userNamespaces]}
- ${'User'} | ${true} | ${true} | ${[...userNamespaces]}
- ${'namespace'} | ${true} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
- ${'sub'} | ${false} | ${false} | ${[...userNamespaces, ...groupNamespaces]}
- `(
- 'with term=$term, includeEmptyNamespace=$includeEmptyNamespace, and shouldFilterNamespaces=$shouldFilterNamespaces should show $expectedItems.length',
- async ({ term, includeEmptyNamespace, shouldFilterNamespaces, expectedItems }) => {
- wrapper = createComponent({
- includeEmptyNamespace,
- emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
- shouldFilterNamespaces,
- });
-
- search(term);
-
- await nextTick();
-
- const expected = expectedItems.map((x) => x.humanName);
-
- expect(findDropdownItemsTexts()).toEqual(expected);
- },
- );
- });
-
- describe('when search is typed in', () => {
- it('emits `search` event', async () => {
- wrapper = createComponent();
-
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
-
- await nextTick();
-
- expect(wrapper.emitted('search')).toEqual([['foo']]);
- });
- });
-
- describe('with a selected namespace', () => {
- const selectedGroupIndex = 1;
- const selectedItem = groupNamespaces[selectedGroupIndex];
-
- beforeEach(() => {
- wrapper = createComponent();
-
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
- findGroupDropdownItems().at(selectedGroupIndex).vm.$emit('click');
- });
-
- it('sets the dropdown text', () => {
- expect(findDropdownText()).toBe(selectedItem.humanName);
- });
-
- it('emits the `select` event when a namespace is selected', () => {
- const args = [selectedItem];
- expect(wrapper.emitted('select')).toEqual([args]);
- });
-
- it('clears search', () => {
- expect(wrapper.findComponent(GlSearchBoxByType).props('value')).toBe('');
- });
- });
-
- describe('with an empty namespace option', () => {
- beforeEach(() => {
- wrapper = createComponent({
- includeEmptyNamespace: true,
- emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
- });
- });
-
- it('includes the empty namespace', () => {
- const first = findDropdownItems().at(0);
-
- expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE);
- });
-
- it('emits the `select` event when a namespace is selected', () => {
- findDropdownItems().at(0).vm.$emit('click');
-
- expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
- });
-
- it.each`
- desc | term | shouldShow
- ${'should hide empty option'} | ${'group'} | ${false}
- ${'should show empty option'} | ${'Empty'} | ${true}
- `('when search for $term, $desc', async ({ term, shouldShow }) => {
- search(term);
-
- await nextTick();
-
- expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
- });
- });
-
- describe('when `hasNextPageOfGroups` prop is `true`', () => {
- it('renders `GlIntersectionObserver` and emits `load-more-groups` event when bottom is reached', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true });
-
- const intersectionObserver = wrapper.findComponent(GlIntersectionObserver);
-
- intersectionObserver.vm.$emit('appear');
-
- expect(intersectionObserver.exists()).toBe(true);
- expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
- });
-
- describe('when `isLoading` prop is `true`', () => {
- it('renders a loading icon', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true, isLoading: true });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
- });
-
- describe('when `isSearchLoading` prop is `true`', () => {
- it('sets `isLoading` prop to `true`', () => {
- wrapper = createComponent({ isSearchLoading: true });
-
- expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
- });
- });
-
- describe('when dropdown is opened', () => {
- it('emits `show` event', () => {
- wrapper = createComponent();
-
- findDropdown().vm.$emit('show');
-
- expect(wrapper.emitted('show')).toEqual([[]]);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index 9b1316677d7..d531147c0e6 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -37,6 +37,7 @@ const mockProps = {
dropdownButtonTitle: 'Move issuable',
dropdownHeaderTitle: 'Move issuable',
moveInProgress: false,
+ disabled: false,
};
const mockEvent = {
@@ -44,20 +45,21 @@ const mockEvent = {
preventDefault: jest.fn(),
};
-const createComponent = (propsData = mockProps) =>
- shallowMount(IssuableMoveDropdown, {
- propsData,
- });
-
describe('IssuableMoveDropdown', () => {
let mock;
let wrapper;
- beforeEach(() => {
- mock = new MockAdapter(axios);
- wrapper = createComponent();
+ const createComponent = (propsData = mockProps) => {
+ wrapper = shallowMount(IssuableMoveDropdown, {
+ propsData,
+ });
wrapper.vm.$refs.dropdown.hide = jest.fn();
wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent();
});
afterEach(() => {
@@ -194,6 +196,12 @@ describe('IssuableMoveDropdown', () => {
expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true);
});
+ it('renders disabled dropdown when `disabled` is true', () => {
+ createComponent({ ...mockProps, disabled: true });
+
+ expect(findDropdownEl().attributes('disabled')).toBe('true');
+ });
+
it('renders header element', () => {
const headerEl = findDropdownEl().find('[data-testid="header"]');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index b58c44645d6..74ddd07d041 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -49,7 +49,6 @@ describe('LabelsSelectRoot', () => {
issuableType = IssuableType.Issue,
queryHandler = successfulQueryHandler,
mutationHandler = successfulMutationHandler,
- isRealtimeEnabled = false,
} = {}) => {
const mockApollo = createMockApollo([
[issueLabelsQuery, queryHandler],
@@ -74,9 +73,6 @@ describe('LabelsSelectRoot', () => {
allowLabelEdit: true,
allowLabelCreate: true,
labelsManagePath: 'test',
- glFeatures: {
- realtimeLabels: isRealtimeEnabled,
- },
},
});
};
@@ -204,17 +200,10 @@ describe('LabelsSelectRoot', () => {
});
});
- it('does not emit `updateSelectedLabels` event when the subscription is triggered and FF is disabled', async () => {
+ it('emits `updateSelectedLabels` event when the subscription is triggered', async () => {
createComponent();
await waitForPromises();
- expect(wrapper.emitted('updateSelectedLabels')).toBeUndefined();
- });
-
- it('emits `updateSelectedLabels` event when the subscription is triggered and FF is enabled', async () => {
- createComponent({ isRealtimeEnabled: true });
- await waitForPromises();
-
expect(wrapper.emitted('updateSelectedLabels')).toEqual([
[
{
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 8dc3348acfa..d720574ce6d 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
@@ -2,6 +2,9 @@ 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';
+
+jest.mock('~/lib/utils/common_utils');
const DEFAULT_PROPS = {
chunkIndex: 2,
@@ -13,11 +16,17 @@ const DEFAULT_PROPS = {
blamePath: 'blame/file.js',
};
+const hash = '#L142';
+
describe('Chunk component', () => {
let wrapper;
+ let idleCallbackSpy;
const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(Chunk, { propsData: { ...DEFAULT_PROPS, ...props } });
+ wrapper = shallowMountExtended(Chunk, {
+ mocks: { $route: { hash } },
+ propsData: { ...DEFAULT_PROPS, ...props },
+ });
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
@@ -26,6 +35,7 @@ describe('Chunk component', () => {
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
createComponent();
});
@@ -51,18 +61,30 @@ describe('Chunk component', () => {
});
describe('rendering', () => {
+ it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ jest.clearAllMocks();
+ createComponent({ isFirstChunk: true });
+
+ expect(window.requestIdleCallback).not.toHaveBeenCalled();
+ expect(findContent().exists()).toBe(true);
+ });
+
it('does not render a Chunk Line component if isHighlighted is false', () => {
expect(findChunkLines().length).toBe(0);
});
+ it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ idleCallbackSpy.mockRestore();
+ createComponent();
+
+ expect(findLineNumbers()).toHaveLength(0);
+ expect(findContent().exists()).toBe(false);
+ });
+
it('renders simplified line numbers and content if isHighlighted is false', () => {
expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
- expect(findLineNumbers().at(0).attributes()).toMatchObject({
- 'data-line-number': `${DEFAULT_PROPS.startingFrom + 1}`,
- href: `#L${DEFAULT_PROPS.startingFrom + 1}`,
- id: `L${DEFAULT_PROPS.startingFrom + 1}`,
- });
+ expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
expect(findContent().text()).toBe(DEFAULT_PROPS.content);
});
@@ -80,5 +102,14 @@ describe('Chunk component', () => {
blamePath: DEFAULT_PROPS.blamePath,
});
});
+
+ it('does not scroll to route hash if last chunk is not loaded', () => {
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+
+ it('scrolls to route hash if last chunk is loaded', () => {
+ createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
+ expect(scrollToElement).toHaveBeenCalledWith(hash, { behavior: 'auto' });
+ });
});
});
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 375b1307616..a7b55d7332f 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
@@ -1,10 +1,26 @@
import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker';
+import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker';
+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 linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
-import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT, GEMSPEC_FILE_TYPE } from './mock_data';
+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,
+} from './mock_data';
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker');
+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');
describe('Highlight.js plugin for linking dependencies', () => {
const hljsResultMock = { value: 'test' };
@@ -18,4 +34,24 @@ describe('Highlight.js plugin for linking dependencies', () => {
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();
+ });
});
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 aa874c9c081..5455479ec71 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
@@ -1,4 +1,34 @@
export const PACKAGE_JSON_FILE_TYPE = 'package_json';
+
export const PACKAGE_JSON_CONTENT = '{ "dependencies": { "@babel/core": "^7.18.5" } }';
+export const COMPOSER_JSON_EXAMPLES = {
+ packagist: '{ "require": { "composer/installers": "^1.2" } }',
+ drupal: '{ "require": { "drupal/bootstrap": "3.x-dev" } }',
+ withoutLink: '{ "require": { "drupal/erp_common": "dev-master" } }',
+};
+
export const GEMSPEC_FILE_TYPE = 'gemspec';
+
+export const GODEPS_JSON_FILE_TYPE = 'godeps_json';
+
+export const GEMFILE_FILE_TYPE = 'gemfile';
+
+export const PODSPEC_JSON_FILE_TYPE = 'podspec_json';
+
+export const PODSPEC_JSON_CONTENT = `{
+ "dependencies": {
+ "MyCheckCore": [
+ ]
+ },
+ "subspecs": [
+ {
+ "dependencies": {
+ "AFNetworking/Security": [
+ ]
+ }
+ }
+ ]
+ }`;
+
+export const COMPOSER_JSON_FILE_TYPE = 'composer_json';
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js
new file mode 100644
index 00000000000..3ecb16ddcd0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js
@@ -0,0 +1,38 @@
+import composerJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker';
+import { COMPOSER_JSON_EXAMPLES } from '../mock_data';
+
+describe('Highlight.js plugin for linking composer.json dependencies', () => {
+ it('mutates the input value by wrapping dependency names and versions in anchors', () => {
+ const inputValue =
+ '<span class="hljs-attr">&quot;drupal/erp_common"&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;dev-master&quot;</span>';
+ const outputValue =
+ '<span class="hljs-attr">&quot;drupal/erp_common"&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;dev-master&quot;</span>';
+ const hljsResultMock = { value: inputValue };
+
+ const output = composerJsonLinker(hljsResultMock, COMPOSER_JSON_EXAMPLES.withoutLink);
+ expect(output).toBe(outputValue);
+ });
+});
+
+const getInputValue = (dependencyString, version) =>
+ `<span class="hljs-attr">&quot;${dependencyString}&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;${version}&quot;</span>`;
+const getOutputValue = (dependencyString, version, expectedHref) =>
+ `<span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${dependencyString}</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${version}</a>&quot;</span>`;
+
+describe('Highlight.js plugin for linking Godeps.json dependencies', () => {
+ it.each`
+ type | dependency | version | expectedHref
+ ${'packagist'} | ${'composer/installers'} | ${'^1.2'} | ${'https://packagist.org/packages/composer/installers'}
+ ${'drupal'} | ${'drupal/bootstrap'} | ${'3.x-dev'} | ${'https://www.drupal.org/project/bootstrap'}
+ `(
+ 'mutates the input value by wrapping dependency names in anchors and altering path when needed',
+ ({ type, dependency, version, expectedHref }) => {
+ const inputValue = getInputValue(dependency, version);
+ const outputValue = getOutputValue(dependency, version, expectedHref);
+ const hljsResultMock = { value: inputValue };
+
+ const output = composerJsonLinker(hljsResultMock, COMPOSER_JSON_EXAMPLES[type]);
+ expect(output).toBe(outputValue);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
index e4ce07ec668..66e2020da27 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
@@ -1,13 +1,15 @@
import {
createLink,
generateHLJSOpenTag,
+ getObjectKeysByKeyName,
} from '~/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util';
+import { PODSPEC_JSON_CONTENT } from '../mock_data';
describe('createLink', () => {
it('generates a link with the correct attributes', () => {
const href = 'http://test.com';
const innerText = 'testing';
- const result = `<a href="${href}" rel="nofollow noreferrer noopener">${innerText}</a>`;
+ const result = `<a href="${href}" target="_blank" rel="nofollow noreferrer noopener">${innerText}</a>`;
expect(createLink(href, innerText)).toBe(result);
});
@@ -18,7 +20,7 @@ describe('createLink', () => {
const escapedHref = '&lt;script&gt;XSS&lt;/script&gt;';
const href = `http://test.com/${unescapedXSS}`;
const innerText = `testing${unescapedXSS}`;
- const result = `<a href="http://test.com/${escapedHref}" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
+ const result = `<a href="http://test.com/${escapedHref}" target="_blank" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
expect(createLink(href, innerText)).toBe(result);
});
@@ -32,3 +34,11 @@ describe('generateHLJSOpenTag', () => {
expect(generateHLJSOpenTag(type)).toBe(result);
});
});
+
+describe('getObjectKeysByKeyName method', () => {
+ it('gets all object keys within specified key', () => {
+ const acc = [];
+ const keys = getObjectKeysByKeyName(JSON.parse(PODSPEC_JSON_CONTENT), 'dependencies', acc);
+ expect(keys).toEqual(['MyCheckCore', 'AFNetworking/Security']);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js
new file mode 100644
index 00000000000..4e188c9af7e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js
@@ -0,0 +1,13 @@
+import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker';
+
+describe('Highlight.js plugin for linking gemfile dependencies', () => {
+ it('mutates the input value by wrapping dependency names in anchors', () => {
+ const inputValue = 'gem </span><span class="hljs-string">&#39;paranoia&#39;';
+ const outputValue =
+ 'gem </span><span class="hljs-string">&#39;<a href="https://rubygems.org/gems/paranoia" target="_blank" rel="nofollow noreferrer noopener">paranoia</a>&#39;';
+ const hljsResultMock = { value: inputValue };
+
+ const output = gemfileLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
index 3f74bfa117f..4b104b0bf43 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
@@ -5,7 +5,7 @@ describe('Highlight.js plugin for linking gemspec dependencies', () => {
const inputValue =
's.add_dependency(<span class="hljs-string">&#x27;rugged&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
const outputValue =
- 's.add_dependency(<span class="hljs-string linked">&#x27;<a href="https://rubygems.org/gems/rugged" rel="nofollow noreferrer noopener">rugged</a>&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
+ 's.add_dependency(<span class="hljs-string linked">&#x27;<a href="https://rubygems.org/gems/rugged" target="_blank" rel="nofollow noreferrer noopener">rugged</a>&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
const hljsResultMock = { value: inputValue };
const output = gemspecLinker(hljsResultMock);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js
new file mode 100644
index 00000000000..ea7e3936846
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js
@@ -0,0 +1,27 @@
+import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
+
+const getInputValue = (dependencyString) =>
+ `<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">&quot;${dependencyString}&quot;</span>`;
+const getOutputValue = (dependencyString, expectedHref) =>
+ `<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${dependencyString}</a>&quot;</span>`;
+
+describe('Highlight.js plugin for linking Godeps.json dependencies', () => {
+ it.each`
+ dependency | expectedHref
+ ${'gitlab.com/group/project/path'} | ${'https://gitlab.com/group/project/_/tree/master/path'}
+ ${'gitlab.com/group/subgroup/project.git/path'} | ${'https://gitlab.com/group/subgroup/_/tree/master/project.git/path'}
+ ${'github.com/docker/docker/pkg/homedir'} | ${'https://github.com/docker/docker/tree/master/pkg/homedir'}
+ ${'golang.org/x/net/http2'} | ${'https://godoc.org/golang.org/x/net/http2'}
+ ${'gopkg.in/yaml.v1'} | ${'https://gopkg.in/yaml.v1'}
+ `(
+ 'mutates the input value by wrapping dependency names in anchors and altering path when needed',
+ ({ dependency, expectedHref }) => {
+ const inputValue = getInputValue(dependency);
+ const outputValue = getOutputValue(dependency, expectedHref);
+ const hljsResultMock = { value: inputValue };
+
+ const output = godepsJsonLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
index e83c129818c..170a44f8ee2 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
@@ -6,7 +6,7 @@ describe('Highlight.js plugin for linking package.json dependencies', () => {
const inputValue =
'<span class="hljs-attr">&quot;@babel/core&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^7.18.5&quot;</span>';
const outputValue =
- '<span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">@babel/core</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">^7.18.5</a>&quot;</span>';
+ '<span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" target="_blank" rel="nofollow noreferrer noopener">@babel/core</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" target="_blank" rel="nofollow noreferrer noopener">^7.18.5</a>&quot;</span>';
const hljsResultMock = { value: inputValue };
const output = packageJsonLinker(hljsResultMock, PACKAGE_JSON_CONTENT);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js
new file mode 100644
index 00000000000..0ef63de68c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js
@@ -0,0 +1,14 @@
+import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker';
+import { PODSPEC_JSON_CONTENT } from '../mock_data';
+
+describe('Highlight.js plugin for linking podspec_json dependencies', () => {
+ it('mutates the input value by wrapping dependency names in anchors', () => {
+ const inputValue =
+ '<span class="hljs-attr">&quot;AFNetworking/Security&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">[';
+ const outputValue =
+ '<span class="hljs-attr">&quot;<a href="https://cocoapods.org/pods/AFNetworking" target="_blank" rel="nofollow noreferrer noopener">AFNetworking/Security</a>&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">[';
+ const hljsResultMock = { value: inputValue };
+ const output = podspecJsonLinker(hljsResultMock, PODSPEC_JSON_CONTENT);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
index bc6df1a2565..8d072c8c8de 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
@@ -8,13 +8,14 @@ describe('Highlight.js plugin for wrapping _emitter nodes', () => {
children: [
{ kind: 'string', children: ['Text 1'] },
{ kind: 'string', children: ['Text 2', { kind: 'comment', children: ['Text 3'] }] },
+ { kind: undefined, sublanguage: true, children: ['Text 3 (sublanguage)'] },
'Text4\nText5',
],
},
},
};
- const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text4</span>\n<span class="">Text5</span>`;
+ const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text 3 (sublanguage)</span><span class="">Text4</span>\n<span class="">Text5</span>`;
wrapChildNodes(hljsResultMock);
expect(hljsResultMock.value).toBe(outputValue);
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 6d319b37b02..33f370efdfa 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -10,6 +10,7 @@ import {
EVENT_LABEL_VIEWER,
EVENT_LABEL_FALLBACK,
ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
} from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
@@ -121,6 +122,7 @@ describe('Source Viewer component', () => {
it('highlights the first chunk', () => {
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
+ expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
});
describe('auto-detects if a language cannot be loaded', () => {
@@ -133,45 +135,27 @@ describe('Source Viewer component', () => {
});
describe('rendering', () => {
- it('renders the first chunk', async () => {
- const firstChunk = findChunks().at(0);
-
- expect(firstChunk.props('content')).toContain(chunk1);
-
- expect(firstChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 0,
+ it.each`
+ chunkIndex | chunkContent | totalChunks
+ ${0} | ${chunk1} | ${0}
+ ${1} | ${chunk2} | ${3}
+ ${2} | ${chunk3Result} | ${3}
+ `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
+ const chunk = findChunks().at(chunkIndex);
+
+ expect(chunk.props('content')).toContain(chunkContent.trim());
+
+ expect(chunk.props()).toMatchObject({
+ totalLines: LINES_PER_CHUNK,
+ startingFrom: LINES_PER_CHUNK * chunkIndex,
+ totalChunks,
});
});
- it('renders the second chunk', async () => {
- const secondChunk = findChunks().at(1);
-
- expect(secondChunk.props('content')).toContain(chunk2.trim());
-
- expect(secondChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 70,
- });
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
});
-
- it('renders the third chunk', async () => {
- const thirdChunk = findChunks().at(2);
-
- expect(thirdChunk.props('content')).toContain(chunk3Result.trim());
-
- expect(chunk3Result).toEqual(chunk3.replace(/\r?\n/g, '\n'));
-
- expect(thirdChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 140,
- });
- });
- });
-
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
});
describe('LineHighlighter', () => {
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 f55d3156581..e1c6020686c 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
@@ -543,24 +543,6 @@ describe('IssuableItem', () => {
});
});
- describe('when issuable was created within the past 24 hours', () => {
- it('renders issuable card with a recently-created style', () => {
- wrapper = createComponent({
- issuable: { ...mockIssuable, createdAt: '2020-12-10T12:34:56' },
- });
-
- expect(wrapper.classes()).toContain('today');
- });
- });
-
- describe('when issuable was created earlier than the past 24 hours', () => {
- it('renders issuable card without a recently-created style', () => {
- wrapper = createComponent({ issuable: { ...mockIssuable, createdAt: '2020-12-09' } });
-
- expect(wrapper.classes()).not.toContain('today');
- });
- });
-
describe('scoped labels', () => {
describe.each`
description | labelPosition | hasScopedLabelsFeature | scoped
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 0c53f599d55..371844e66f4 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -322,6 +322,18 @@ describe('IssuableListRoot', () => {
});
});
+ describe('showFilteredSearchFriendlyText prop', () => {
+ describe.each([true, false])('when %s', (showFilteredSearchFriendlyText) => {
+ it('passes its value to FilteredSearchBar', () => {
+ wrapper = createComponent({ props: { showFilteredSearchFriendlyText } });
+
+ expect(findFilteredSearchBar().props('showFriendlyText')).toBe(
+ showFilteredSearchFriendlyText,
+ );
+ });
+ });
+ });
+
describe('alert', () => {
const error = 'oopsie!';
diff --git a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
new file mode 100644
index 00000000000..3dbff024a6b
--- /dev/null
+++ b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
@@ -0,0 +1,453 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Webhook push events form editor component Different push events rules when editing existing hook with "all_branches" strategy selected 1`] = `
+<gl-form-radio-group-stub
+ checked="all_branches"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing existing hook with "regex" strategy selected 1`] = `
+<gl-form-radio-group-stub
+ checked="regex"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value="foo"
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Regex such as %{REGEX_CODE} is supported."
+ />
+ </p>
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing existing hook with "wildcard" strategy selected 1`] = `
+<gl-form-radio-group-stub
+ checked="wildcard"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value="foo"
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported."
+ />
+ </p>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing new hook all_branches should be selected by default 1`] = `
+<gl-form-radio-group-stub
+ checked="all_branches"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing new hook should be able to set regex rule 1`] = `
+<gl-form-radio-group-stub
+ checked="regex"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value=""
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Regex such as %{REGEX_CODE} is supported."
+ />
+ </p>
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing new hook should be able to set wildcard rule 1`] = `
+<gl-form-radio-group-stub
+ checked="wildcard"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value=""
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported."
+ />
+ </p>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js
index 16e0a3f549e..45a39d2dd58 100644
--- a/spec/frontend/webhooks/components/form_url_app_spec.js
+++ b/spec/frontend/webhooks/components/form_url_app_spec.js
@@ -1,10 +1,14 @@
import { nextTick } from 'vue';
-import { GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { scrollToElement } from '~/lib/utils/common_utils';
import FormUrlApp from '~/webhooks/components/form_url_app.vue';
import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/common_utils');
describe('FormUrlApp', () => {
let wrapper;
@@ -26,8 +30,11 @@ describe('FormUrlApp', () => {
const findAllUrlMaskItems = () => wrapper.findAllComponents(FormUrlMaskItem);
const findAddItem = () => wrapper.findComponent(GlLink);
const findFormUrl = () => wrapper.findByTestId('form-url');
+ const findFormUrlGroup = () => wrapper.findAllComponents(GlFormGroup).at(0);
const findFormUrlPreview = () => wrapper.findByTestId('form-url-preview');
const findUrlMaskSection = () => wrapper.findByTestId('url-mask-section');
+ const findFormEl = () => document.querySelector('.js-webhook-form');
+ const submitForm = () => findFormEl().dispatchEvent(new Event('submit'));
describe('template', () => {
it('renders radio buttons for URL masking', () => {
@@ -60,8 +67,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const firstItem = findAllUrlMaskItems().at(0);
- expect(firstItem.props('itemKey')).toBeNull();
- expect(firstItem.props('itemValue')).toBeNull();
+ expect(firstItem.props()).toMatchObject({
+ itemKey: null,
+ itemValue: null,
+ });
});
});
@@ -90,12 +99,18 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(2);
const firstItem = findAllUrlMaskItems().at(0);
- expect(firstItem.props('itemKey')).toBe(mockItem1.key);
- expect(firstItem.props('itemValue')).toBe(mockItem1.value);
+ expect(firstItem.props()).toMatchObject({
+ itemKey: mockItem1.key,
+ itemValue: mockItem1.value,
+ isEditing: true,
+ });
const secondItem = findAllUrlMaskItems().at(1);
- expect(secondItem.props('itemKey')).toBe(mockItem2.key);
- expect(secondItem.props('itemValue')).toBe(mockItem2.value);
+ expect(secondItem.props()).toMatchObject({
+ itemKey: mockItem2.key,
+ itemValue: mockItem2.value,
+ isEditing: true,
+ });
});
describe('on mask item input', () => {
@@ -106,8 +121,10 @@ describe('FormUrlApp', () => {
firstItem.vm.$emit('input', mockInput);
await nextTick();
- expect(firstItem.props('itemKey')).toBe(mockInput.key);
- expect(firstItem.props('itemValue')).toBe(mockInput.value);
+ expect(firstItem.props()).toMatchObject({
+ itemKey: mockInput.key,
+ itemValue: mockInput.value,
+ });
});
});
@@ -119,8 +136,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(3);
const lastItem = findAllUrlMaskItems().at(-1);
- expect(lastItem.props('itemKey')).toBeNull();
- expect(lastItem.props('itemValue')).toBeNull();
+ expect(lastItem.props()).toMatchObject({
+ itemKey: null,
+ itemValue: null,
+ });
});
});
@@ -133,8 +152,88 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const newFirstItem = findAllUrlMaskItems().at(0);
- expect(newFirstItem.props('itemKey')).toBe(mockItem2.key);
- expect(newFirstItem.props('itemValue')).toBe(mockItem2.value);
+ expect(newFirstItem.props()).toMatchObject({
+ itemKey: mockItem2.key,
+ itemValue: mockItem2.value,
+ });
+ });
+ });
+ });
+
+ describe('validations', () => {
+ const inputRequiredText = FormUrlApp.i18n.inputRequired;
+
+ beforeEach(() => {
+ setHTMLFixture('<form class="js-webhook-form"></form>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it.each`
+ url | state | scrollToElementCalls
+ ${null} | ${undefined} | ${1}
+ ${''} | ${undefined} | ${1}
+ ${'https://example.com/'} | ${'true'} | ${0}
+ `('when URL is `$url`, state is `$state`', async ({ url, state, scrollToElementCalls }) => {
+ createComponent({
+ props: { initialUrl: url },
+ });
+
+ submitForm();
+ await nextTick();
+
+ expect(findFormUrlGroup().attributes('state')).toBe(state);
+ expect(scrollToElement).toHaveBeenCalledTimes(scrollToElementCalls);
+ expect(findFormUrlGroup().attributes('invalid-feedback')).toBe(inputRequiredText);
+ });
+
+ it.each`
+ key | value | keyInvalidFeedback | valueInvalidFeedback | scrollToElementCalls
+ ${null} | ${null} | ${inputRequiredText} | ${inputRequiredText} | ${1}
+ ${null} | ${'random'} | ${inputRequiredText} | ${FormUrlApp.i18n.valuePartOfUrl} | ${1}
+ ${null} | ${'secret'} | ${inputRequiredText} | ${null} | ${1}
+ ${'key'} | ${null} | ${null} | ${inputRequiredText} | ${1}
+ ${'key'} | ${'secret'} | ${null} | ${null} | ${0}
+ `(
+ 'when key is `$key` and value is `$value`',
+ async ({ key, value, keyInvalidFeedback, valueInvalidFeedback, scrollToElementCalls }) => {
+ createComponent({
+ props: { initialUrl: 'http://example.com?password=secret' },
+ });
+ findRadioGroup().vm.$emit('input', true);
+ await nextTick();
+
+ const maskItem = findAllUrlMaskItems().at(0);
+ const mockInput = { index: 0, key, value };
+ maskItem.vm.$emit('input', mockInput);
+
+ submitForm();
+ await nextTick();
+
+ expect(maskItem.props('keyInvalidFeedback')).toBe(keyInvalidFeedback);
+ expect(maskItem.props('valueInvalidFeedback')).toBe(valueInvalidFeedback);
+ expect(scrollToElement).toHaveBeenCalledTimes(scrollToElementCalls);
+ },
+ );
+
+ describe('when initialUrlVariables is passed', () => {
+ it('does not validate empty values', async () => {
+ const initialUrlVariables = [{ key: 'key' }];
+
+ createComponent({
+ props: { initialUrl: 'url', initialUrlVariables },
+ });
+
+ submitForm();
+ await nextTick();
+
+ const maskItem = findAllUrlMaskItems().at(0);
+
+ expect(maskItem.props('keyInvalidFeedback')).toBeNull();
+ expect(maskItem.props('valueInvalidFeedback')).toBeNull();
+ expect(scrollToElement).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/webhooks/components/form_url_mask_item_spec.js b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
index ab028ef2997..06c743749a6 100644
--- a/spec/frontend/webhooks/components/form_url_mask_item_spec.js
+++ b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
@@ -14,6 +14,7 @@ describe('FormUrlMaskItem', () => {
const mockKey = 'key';
const mockValue = 'value';
const mockInput = 'input';
+ const mockFeedback = 'feedback';
const createComponent = ({ props } = {}) => {
wrapper = shallowMountExtended(FormUrlMaskItem, {
@@ -21,29 +22,80 @@ describe('FormUrlMaskItem', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findMaskItemKey = () => wrapper.findByTestId('mask-item-key');
const findMaskItemValue = () => wrapper.findByTestId('mask-item-value');
const findRemoveButton = () => wrapper.findComponent(GlButton);
describe('template', () => {
it('renders input for key and value', () => {
- createComponent();
+ createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
const keyInput = findMaskItemKey();
- expect(keyInput.attributes('label')).toBe(FormUrlMaskItem.i18n.keyLabel);
- expect(keyInput.findComponent(GlFormInput).attributes('name')).toBe(
- 'hook[url_variables][][key]',
- );
+ expect(keyInput.attributes()).toMatchObject({
+ label: FormUrlMaskItem.i18n.keyLabel,
+ state: 'true',
+ });
+ expect(keyInput.findComponent(GlFormInput).attributes()).toMatchObject({
+ name: 'hook[url_variables][][key]',
+ value: mockKey,
+ });
const valueInput = findMaskItemValue();
- expect(valueInput.attributes('label')).toBe(FormUrlMaskItem.i18n.valueLabel);
- expect(valueInput.findComponent(GlFormInput).attributes('name')).toBe(
- 'hook[url_variables][][value]',
- );
+ expect(valueInput.attributes()).toMatchObject({
+ label: FormUrlMaskItem.i18n.valueLabel,
+ state: 'true',
+ });
+ expect(valueInput.findComponent(GlFormInput).attributes()).toMatchObject({
+ name: 'hook[url_variables][][value]',
+ value: mockValue,
+ });
+ });
+
+ describe('when isEditing is true', () => {
+ beforeEach(() => {
+ createComponent({ props: { isEditing: true } });
+ });
+
+ it('renders disabled key and value', () => {
+ expect(findMaskItemKey().findComponent(GlFormInput).attributes('disabled')).toBe('true');
+ expect(findMaskItemValue().findComponent(GlFormInput).attributes('disabled')).toBe('true');
+ });
+
+ it('renders disabled remove button', () => {
+ expect(findRemoveButton().attributes('disabled')).toBe('true');
+ });
+
+ it('displays ************ as input value', () => {
+ expect(findMaskItemValue().findComponent(GlFormInput).attributes('value')).toBe(
+ '************',
+ );
+ });
+ });
+
+ describe('when keyInvalidFeedback is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { keyInvalidFeedback: mockFeedback },
+ });
+ });
+
+ it('sets validation message on key', () => {
+ expect(findMaskItemKey().attributes('invalid-feedback')).toBe(mockFeedback);
+ expect(findMaskItemKey().attributes('state')).toBeUndefined();
+ });
+ });
+
+ describe('when valueInvalidFeedback is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { valueInvalidFeedback: mockFeedback },
+ });
+ });
+
+ it('sets validation message on value', () => {
+ expect(findMaskItemValue().attributes('invalid-feedback')).toBe(mockFeedback);
+ expect(findMaskItemValue().attributes('state')).toBeUndefined();
+ });
});
describe('on key input', () => {
diff --git a/spec/frontend/webhooks/components/push_events_spec.js b/spec/frontend/webhooks/components/push_events_spec.js
new file mode 100644
index 00000000000..ccb61c4049a
--- /dev/null
+++ b/spec/frontend/webhooks/components/push_events_spec.js
@@ -0,0 +1,117 @@
+import { nextTick } from 'vue';
+import { GlFormCheckbox, GlFormRadioGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PushEvents from '~/webhooks/components/push_events.vue';
+
+describe('Webhook push events form editor component', () => {
+ let wrapper;
+
+ const findPushEventsCheckBox = (w = wrapper) => w.findComponent(GlFormCheckbox);
+ const findPushEventsIndicator = (w = wrapper) => w.find('input[name="hook[push_events]"]');
+ const findPushEventRulesGroup = (w = wrapper) => w.findComponent(GlFormRadioGroup);
+ const getPushEventsRuleValue = (w = wrapper) => findPushEventRulesGroup(w).vm.$attrs.checked;
+ const findWildcardRuleInput = (w = wrapper) => w.findByTestId('webhook_branch_filter_field');
+ const findRegexRuleInput = (w = wrapper) => w.findByTestId('webhook_branch_filter_field');
+
+ const createComponent = (provides) =>
+ shallowMountExtended(PushEvents, {
+ provide: {
+ isNewHook: true,
+ pushEvents: false,
+ strategy: 'wildcard',
+ pushEventsBranchFilter: '',
+ ...provides,
+ },
+ });
+
+ describe('Renders push events checkbox', () => {
+ it('when it is a new hook', async () => {
+ wrapper = createComponent({
+ isNewHook: true,
+ });
+ await nextTick();
+
+ const checkbox = findPushEventsCheckBox();
+ expect(checkbox.exists()).toBe(true);
+ expect(findPushEventRulesGroup().exists()).toBe(false);
+ expect(findPushEventsIndicator().attributes('value')).toBe('false');
+ });
+
+ it('when it is not a new hook and push events is enabled', async () => {
+ wrapper = createComponent({
+ isNewHook: false,
+ pushEvents: true,
+ });
+ await nextTick();
+
+ expect(findPushEventsCheckBox().exists()).toBe(true);
+ expect(findPushEventRulesGroup().exists()).toBe(true);
+ expect(findPushEventsIndicator().attributes('value')).toBe('true');
+ });
+ });
+
+ describe('Different push events rules', () => {
+ describe('when editing new hook', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ isNewHook: true,
+ });
+ await nextTick();
+ await findPushEventsCheckBox().vm.$emit('input', true);
+ await nextTick();
+ });
+
+ it('all_branches should be selected by default', async () => {
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+ });
+
+ it('should be able to set wildcard rule', async () => {
+ expect(getPushEventsRuleValue()).toBe('all_branches');
+ expect(findWildcardRuleInput().exists()).toBe(false);
+ expect(findRegexRuleInput().exists()).toBe(false);
+
+ await findPushEventRulesGroup(wrapper).vm.$emit('input', 'wildcard');
+ expect(findWildcardRuleInput().exists()).toBe(true);
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+
+ const testVal = 'test-val';
+ findWildcardRuleInput().vm.$emit('input', testVal);
+ await nextTick();
+ expect(findWildcardRuleInput().attributes('value')).toBe(testVal);
+ });
+
+ it('should be able to set regex rule', async () => {
+ expect(getPushEventsRuleValue()).toBe('all_branches');
+ expect(findRegexRuleInput().exists()).toBe(false);
+ expect(findWildcardRuleInput().exists()).toBe(false);
+
+ await findPushEventRulesGroup(wrapper).vm.$emit('input', 'regex');
+ expect(findRegexRuleInput().exists()).toBe(true);
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+
+ const testVal = 'test-val';
+ findRegexRuleInput().vm.$emit('input', testVal);
+ await nextTick();
+ expect(findRegexRuleInput().attributes('value')).toBe(testVal);
+ });
+ });
+
+ describe('when editing existing hook', () => {
+ it.each(['all_branches', 'wildcard', 'regex'])(
+ 'with "%s" strategy selected',
+ async (strategy) => {
+ wrapper = createComponent({
+ isNewHook: false,
+ pushEvents: true,
+ pushEventsBranchFilter: 'foo',
+ strategy,
+ });
+ await nextTick();
+
+ expect(findPushEventsIndicator().attributes('value')).toBe('true');
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+ },
+ );
+ });
+ });
+});
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 1b204b6fd60..7367212e49f 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -8,7 +8,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
-import { temporaryConfig } from '~/graphql_shared/issuable_client';
+import { config } from '~/graphql_shared/issuable_client';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -86,7 +86,7 @@ describe('WorkItemAssignees component', () => {
],
{},
{
- typePolicies: temporaryConfig.cacheConfig.typePolicies,
+ typePolicies: config.cacheConfig.typePolicies,
},
);
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
new file mode 100644
index 00000000000..01ab7824975
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
@@ -0,0 +1,108 @@
+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 { descriptionTextWithCheckboxes, descriptionHtmlWithCheckboxes } from '../mock_data';
+
+describe('WorkItemDescription', () => {
+ let wrapper;
+
+ const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
+ const findCheckboxAtIndex = (index) => wrapper.findAll('input[type="checkbox"]').at(index);
+
+ const defaultWorkItemDescription = {
+ description: descriptionTextWithCheckboxes,
+ descriptionHtml: descriptionHtmlWithCheckboxes,
+ };
+
+ const createComponent = ({
+ workItemDescription = defaultWorkItemDescription,
+ canEdit = false,
+ } = {}) => {
+ wrapper = shallowMount(WorkItemDescriptionRendered, {
+ propsData: {
+ workItemDescription,
+ canEdit,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders gfm', async () => {
+ const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
+
+ createComponent();
+
+ await nextTick();
+
+ expect(renderGFMSpy).toHaveBeenCalled();
+ });
+
+ describe('with checkboxes', () => {
+ beforeEach(() => {
+ createComponent({
+ canEdit: true,
+ workItemDescription: {
+ description: `- [x] todo 1\n- [ ] todo 2`,
+ descriptionHtml: `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
+<li class="task-list-item" data-sourcepos="1:1-2:15">
+<input checked="" class="task-list-item-checkbox" type="checkbox"> todo 1</li>
+<li class="task-list-item" data-sourcepos="2:1-2:15">
+<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
+</ul>`,
+ },
+ });
+ });
+
+ it('checks unchecked checkbox', async () => {
+ findCheckboxAtIndex(1).setChecked();
+
+ await nextTick();
+
+ const updatedDescription = `- [x] todo 1\n- [x] todo 2`;
+ expect(wrapper.emitted('descriptionUpdated')).toEqual([[updatedDescription]]);
+ });
+
+ it('disables checkbox while updating', async () => {
+ findCheckboxAtIndex(1).setChecked();
+
+ await nextTick();
+
+ expect(findCheckboxAtIndex(1).attributes().disabled).toBeDefined();
+ });
+
+ it('unchecks checked checkbox', async () => {
+ findCheckboxAtIndex(0).setChecked(false);
+
+ await nextTick();
+
+ const updatedDescription = `- [ ] todo 1\n- [ ] todo 2`;
+ expect(wrapper.emitted('descriptionUpdated')).toEqual([[updatedDescription]]);
+ });
+ });
+
+ describe('Edit button', () => {
+ it('is not visible when canUpdate = false', async () => {
+ await createComponent({
+ canUpdate: false,
+ });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('toggles edit mode', async () => {
+ createComponent({
+ canEdit: true,
+ });
+
+ findEditButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
+ });
+});
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 0691fe25e0d..c79b049442d 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -8,21 +8,23 @@ import EditedAt from '~/issues/show/components/edited.vue';
import { updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
+import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
updateWorkItemMutationResponse,
+ workItemDescriptionSubscriptionResponse,
workItemResponseFactory,
workItemQueryResponse,
+ projectWorkItemResponse,
} from '../mock_data';
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/lib/utils/autosave');
const workItemId = workItemQueryResponse.data.workItem.id;
@@ -33,12 +35,22 @@ describe('WorkItemDescription', () => {
Vue.use(VueApollo);
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
+ const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
+ let workItemResponseHandler;
+ let workItemsMvc2;
- const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+ const findRenderedDescription = () => wrapper.findComponent(WorkItemDescriptionRendered);
const findEditedAt = () => wrapper.findComponent(EditedAt);
- const editDescription = (newText) => wrapper.find('textarea').setValue(newText);
+ const editDescription = (newText) => {
+ if (workItemsMvc2) {
+ return findMarkdownEditor().vm.$emit('input', newText);
+ }
+ return wrapper.find('textarea').setValue(newText);
+ };
const clickCancel = () => wrapper.find('[data-testid="cancel"]').vm.$emit('click');
const clickSave = () => wrapper.find('[data-testid="save-description"]').vm.$emit('click', {});
@@ -48,18 +60,30 @@ describe('WorkItemDescription', () => {
canUpdate = true,
workItemResponse = workItemResponseFactory({ canUpdate }),
isEditing = false,
+ fetchByIid = false,
} = {}) => {
- const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
+ workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
[workItemQuery, workItemResponseHandler],
[updateWorkItemMutation, mutationHandler],
+ [workItemDescriptionSubscription, subscriptionHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
]),
propsData: {
workItemId: id,
fullPath: 'test-project-path',
+ queryVariables: {
+ id: workItemId,
+ },
+ fetchByIid,
+ },
+ provide: {
+ glFeatures: {
+ workItemsMvc2,
+ },
},
stubs: {
MarkdownField,
@@ -69,7 +93,7 @@ describe('WorkItemDescription', () => {
await waitForPromises();
if (isEditing) {
- findEditButton().vm.$emit('click');
+ findRenderedDescription().vm.$emit('startEditing');
await nextTick();
}
@@ -79,171 +103,178 @@ describe('WorkItemDescription', () => {
wrapper.destroy();
});
- describe('Edit button', () => {
- it('is not visible when canUpdate = false', async () => {
- await createComponent({
- canUpdate: false,
+ describe.each([true, false])(
+ 'editing description with workItemsMvc2 %workItemsMvc2Enabled',
+ (workItemsMvc2Enabled) => {
+ beforeEach(() => {
+ beforeEach(() => {
+ workItemsMvc2 = workItemsMvc2Enabled;
+ });
});
- expect(findEditButton().exists()).toBe(false);
- });
+ describe('editing description', () => {
+ it('shows edited by text', async () => {
+ const lastEditedAt = '2022-09-21T06:18:42Z';
+ const lastEditedBy = {
+ name: 'Administrator',
+ webPath: '/root',
+ };
- it('toggles edit mode', async () => {
- await createComponent({
- canUpdate: true,
- });
+ await createComponent({
+ workItemResponse: workItemResponseFactory({
+ lastEditedAt,
+ lastEditedBy,
+ }),
+ });
- findEditButton().vm.$emit('click');
+ expect(findEditedAt().props()).toEqual({
+ updatedAt: lastEditedAt,
+ updatedByName: lastEditedBy.name,
+ updatedByPath: lastEditedBy.webPath,
+ });
+ });
- await nextTick();
+ it('does not show edited by text', async () => {
+ await createComponent();
- expect(findMarkdownField().exists()).toBe(true);
- });
- });
+ expect(findEditedAt().exists()).toBe(false);
+ });
- describe('editing description', () => {
- it('shows edited by text', async () => {
- const lastEditedAt = '2022-09-21T06:18:42Z';
- const lastEditedBy = {
- name: 'Administrator',
- webPath: '/root',
- };
-
- await createComponent({
- workItemResponse: workItemResponseFactory({
- lastEditedAt,
- lastEditedBy,
- }),
- });
+ it('cancels when clicking cancel', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- expect(findEditedAt().props()).toEqual({
- updatedAt: lastEditedAt,
- updatedByName: lastEditedBy.name,
- updatedByPath: lastEditedBy.webPath,
- });
- });
+ clickCancel();
- it('does not show edited by text', async () => {
- await createComponent();
+ await nextTick();
- expect(findEditedAt().exists()).toBe(false);
- });
+ expect(confirmAction).not.toHaveBeenCalled();
+ expect(findMarkdownField().exists()).toBe(false);
+ });
- it('cancels when clicking cancel', async () => {
- await createComponent({
- isEditing: true,
- });
+ it('prompts for confirmation when clicking cancel after changes', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- clickCancel();
+ editDescription('updated desc');
- await nextTick();
+ clickCancel();
- expect(confirmAction).not.toHaveBeenCalled();
- expect(findMarkdownField().exists()).toBe(false);
- });
+ await nextTick();
- it('prompts for confirmation when clicking cancel after changes', async () => {
- await createComponent({
- isEditing: true,
- });
+ expect(confirmAction).toHaveBeenCalled();
+ });
- editDescription('updated desc');
+ it('calls update widgets mutation', async () => {
+ const updatedDesc = 'updated desc';
- clickCancel();
+ await createComponent({
+ isEditing: true,
+ });
- await nextTick();
+ editDescription(updatedDesc);
- expect(confirmAction).toHaveBeenCalled();
- });
+ clickSave();
- it('calls update widgets mutation', async () => {
- await createComponent({
- isEditing: true,
- });
+ await waitForPromises();
- editDescription('updated desc');
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ descriptionWidget: {
+ description: updatedDesc,
+ },
+ },
+ });
+ });
- clickSave();
+ it('tracks editing description', async () => {
+ await createComponent({
+ isEditing: true,
+ markdownPreviewPath: '/preview',
+ });
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await waitForPromises();
+ clickSave();
- expect(mutationSuccessHandler).toHaveBeenCalledWith({
- input: {
- id: workItemId,
- descriptionWidget: {
- description: 'updated desc',
- },
- },
- });
- });
+ await waitForPromises();
- it('tracks editing description', async () => {
- await createComponent({
- isEditing: true,
- markdownPreviewPath: '/preview',
- });
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_description',
+ property: 'type_Task',
+ });
+ });
- clickSave();
+ it('emits error when mutation returns error', async () => {
+ const error = 'eror';
- await waitForPromises();
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: {},
+ errors: [error],
+ },
+ },
+ }),
+ });
- expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_description',
- property: 'type_Task',
- });
- });
+ editDescription('updated desc');
- it('emits error when mutation returns error', async () => {
- const error = 'eror';
+ clickSave();
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockResolvedValue({
- data: {
- workItemUpdate: {
- workItem: {},
- errors: [error],
- },
- },
- }),
- });
+ await waitForPromises();
- editDescription('updated desc');
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- clickSave();
+ it('emits error when mutation fails', async () => {
+ const error = 'eror';
- await waitForPromises();
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
+ });
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ editDescription('updated desc');
- it('emits error when mutation fails', async () => {
- const error = 'eror';
+ clickSave();
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
- });
+ await waitForPromises();
- editDescription('updated desc');
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- clickSave();
+ it('autosaves description', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- await waitForPromises();
+ editDescription('updated desc');
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ expect(updateDraft).toHaveBeenCalled();
+ });
+ });
+
+ it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
+ createComponent({ fetchByIid: false });
+ await waitForPromises();
- it('autosaves description', async () => {
- await createComponent({
- isEditing: true,
+ expect(workItemResponseHandler).toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
});
- editDescription('updated desc');
+ it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
+ createComponent({ fetchByIid: true });
+ await waitForPromises();
- expect(updateDraft).toHaveBeenCalled();
- });
- });
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).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 6b1ef8971d3..4029e47c390 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,6 +86,7 @@ describe('WorkItemDetailModal component', () => {
isModal: true,
workItemId: defaultPropsData.workItemId,
workItemParentId: defaultPropsData.issueGid,
+ iid: 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 aae61b11196..26777b57797 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -24,12 +24,13 @@ import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemInformation from '~/work_items/components/work_item_information.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';
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
+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 { temporaryConfig } from '~/graphql_shared/issuable_client';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockParent,
@@ -37,6 +38,8 @@ import {
workItemResponseFactory,
workItemTitleSubscriptionResponse,
workItemAssigneesSubscriptionResponse,
+ workItemMilestoneSubscriptionResponse,
+ projectWorkItemResponse,
} from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -52,8 +55,12 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const successByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const milestoneSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemMilestoneSubscriptionResponse);
const assigneesSubscriptionHandler = jest
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
@@ -85,26 +92,23 @@ describe('WorkItemDetail component', () => {
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
- includeWidgets = false,
workItemsMvc2Enabled = false,
+ fetchByIid = false,
+ iidPathQueryParam = undefined,
} = {}) => {
const handlers = [
[workItemQuery, handler],
[workItemTitleSubscription, subscriptionHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
+ [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [workItemByIidQuery, successByIidHandler],
confidentialityMock,
];
wrapper = shallowMount(WorkItemDetail, {
- apolloProvider: createMockApollo(
- handlers,
- {},
- {
- typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
- },
- ),
- propsData: { isModal, workItemId },
+ apolloProvider: createMockApollo(handlers),
+ propsData: { isModal, workItemId, iid: '1' },
data() {
return {
updateInProgress,
@@ -114,15 +118,24 @@ describe('WorkItemDetail component', () => {
provide: {
glFeatures: {
workItemsMvc2: workItemsMvc2Enabled,
+ useIidInWorkItemsPath: fetchByIid,
},
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
projectNamespace: 'namespace',
+ fullPath: 'group/project',
},
stubs: {
WorkItemWeight: true,
WorkItemIteration: true,
},
+ mocks: {
+ $route: {
+ query: {
+ iid_path: iidPathQueryParam,
+ },
+ },
+ },
});
};
@@ -421,8 +434,9 @@ describe('WorkItemDetail component', () => {
});
describe('subscriptions', () => {
- it('calls the title subscription', () => {
+ it('calls the title subscription', async () => {
createComponent();
+ await waitForPromises();
expect(titleSubscriptionHandler).toHaveBeenCalledWith({
issuableId: workItemQueryResponse.data.workItem.id,
@@ -543,15 +557,41 @@ describe('WorkItemDetail component', () => {
describe('milestone widget', () => {
it.each`
- description | includeWidgets | exists
- ${'renders when widget is returned from API'} | ${true} | ${true}
- ${'does not render when widget is not returned from API'} | ${false} | ${false}
- `('$description', async ({ includeWidgets, exists }) => {
- createComponent({ includeWidgets, workItemsMvc2Enabled: true });
+ description | milestoneWidgetPresent | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', async ({ milestoneWidgetPresent, exists }) => {
+ const response = workItemResponseFactory({ milestoneWidgetPresent });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
expect(findWorkItemMilestone().exists()).toBe(exists);
});
+
+ describe('milestone subscription', () => {
+ describe('when the milestone widget exists', () => {
+ it('calls the milestone subscription', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
+ });
+
+ describe('when the assignees widget does not exist', () => {
+ it('does not call the milestone subscription', async () => {
+ const response = workItemResponseFactory({ milestoneWidgetPresent: false });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler });
+ await waitForPromises();
+
+ expect(milestoneSubscriptionHandler).not.toHaveBeenCalled();
+ });
+ });
+ });
});
describe('work item information', () => {
@@ -571,4 +611,35 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemInformationAlert().exists()).toBe(false);
});
});
+
+ it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ id: workItemQueryResponse.data.workItem.id,
+ });
+ expect(successByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is true but there is no `iid_path` parameter in URL', async () => {
+ createComponent({ fetchByIid: true });
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ id: workItemQueryResponse.data.workItem.id,
+ });
+ expect(successByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => {
+ createComponent({ fetchByIid: true, iidPathQueryParam: 'true' });
+ await waitForPromises();
+
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).toHaveBeenCalledWith({
+ fullPath: 'group/project',
+ iid: '1',
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js
index 701406b9588..7ebaf8209c7 100644
--- a/spec/frontend/work_items/components/work_item_due_date_spec.js
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -140,7 +140,7 @@ describe('WorkItemDueDate component', () => {
beforeEach(() => {
createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
- datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker.calendar, 'show');
+ datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker, 'show');
findStartDatePicker().vm.$emit('input', startDate);
findStartDatePicker().vm.$emit('close');
});
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 e6ff7e8502d..9f7659b3f8d 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -9,6 +9,7 @@ import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widg
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';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants';
import {
@@ -18,6 +19,7 @@ import {
workItemResponseFactory,
updateWorkItemMutationResponse,
workItemLabelsSubscriptionResponse,
+ projectWorkItemResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -33,6 +35,7 @@ describe('WorkItemLabels component', () => {
const findLabelsTitle = () => wrapper.findByTestId('labels-title');
const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
@@ -45,12 +48,14 @@ describe('WorkItemLabels component', () => {
workItemQueryHandler = workItemQuerySuccess,
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
+ fetchByIid = false,
} = {}) => {
const apolloProvider = createMockApollo([
[workItemQuery, workItemQueryHandler],
[labelSearchQuery, searchQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
[workItemLabelsSubscription, subscriptionHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
]);
wrapper = mountExtended(WorkItemLabels, {
@@ -58,6 +63,10 @@ describe('WorkItemLabels component', () => {
workItemId,
canUpdate,
fullPath: 'test-project-path',
+ queryVariables: {
+ id: workItemId,
+ },
+ fetchByIid,
},
attachTo: document.body,
apolloProvider,
@@ -226,4 +235,20 @@ describe('WorkItemLabels component', () => {
});
});
});
+
+ it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
+ createComponent({ fetchByIid: false });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
+ createComponent({ fetchByIid: true });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).not.toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).toHaveBeenCalled();
+ });
});
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 ab3ea623e3e..071d5fb715a 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
-import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlTokenSelector } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
+import { FORM_TYPES } from '~/work_items/constants';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
@@ -14,6 +15,7 @@ import {
projectWorkItemTypesQueryResponse,
createWorkItemMutationResponse,
updateWorkItemMutationResponse,
+ mockIterationWidgetResponse,
} from '../../mock_data';
Vue.use(VueApollo);
@@ -23,22 +25,35 @@ describe('WorkItemLinksForm', () => {
const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
+ const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
+
+ const mockParentIteration = mockIterationWidgetResponse;
const createComponent = async ({
- listResponse = availableWorkItemsResponse,
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
hasIterationsFeature = false,
+ workItemsMvc2Enabled = false,
+ parentIteration = null,
+ formType = FORM_TYPES.create,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
- [projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)],
+ [projectWorkItemsQuery, availableWorkItemsResolver],
[projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)],
[updateWorkItemMutation, updateMutationResolver],
[createWorkItemMutation, createMutationResolver],
]),
- propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential },
+ propsData: {
+ issuableGid: 'gid://gitlab/WorkItem/1',
+ parentConfidential,
+ parentIteration,
+ formType,
+ },
provide: {
+ glFeatures: {
+ workItemsMvc2: workItemsMvc2Enabled,
+ },
projectPath: 'project/path',
hasIterationsFeature,
},
@@ -48,89 +63,155 @@ describe('WorkItemLinksForm', () => {
};
const findForm = () => wrapper.findComponent(GlForm);
- const findCombobox = () => wrapper.findComponent(GlFormCombobox);
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findInput = () => wrapper.findComponent(GlFormInput);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
- beforeEach(async () => {
- await createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders form', () => {
- expect(findForm().exists()).toBe(true);
- });
-
- it('creates child task in non confidential parent', async () => {
- findInput().vm.$emit('input', 'Create task test');
+ describe('creating a new work item', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
- findForm().vm.$emit('submit', {
- preventDefault: jest.fn(),
+ it('renders create form', () => {
+ expect(findForm().exists()).toBe(true);
+ expect(findInput().exists()).toBe(true);
+ expect(findAddChildButton().text()).toBe('Create task');
+ expect(findTokenSelector().exists()).toBe(false);
});
- await waitForPromises();
- expect(createMutationResolver).toHaveBeenCalledWith({
- input: {
- title: 'Create task test',
- projectPath: 'project/path',
- workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
- hierarchyWidget: {
- parentId: 'gid://gitlab/WorkItem/1',
+
+ it('creates child task in non confidential parent', async () => {
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
},
- confidential: false,
- },
+ });
});
- });
- it('creates child task in confidential parent', async () => {
- await createComponent({ parentConfidential: true });
+ it('creates child task in confidential parent', async () => {
+ await createComponent({ parentConfidential: true });
- findInput().vm.$emit('input', 'Create confidential task');
+ findInput().vm.$emit('input', 'Create confidential task');
- findForm().vm.$emit('submit', {
- preventDefault: jest.fn(),
- });
- await waitForPromises();
- expect(createMutationResolver).toHaveBeenCalledWith({
- input: {
- title: 'Create confidential task',
- projectPath: 'project/path',
- workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
- hierarchyWidget: {
- parentId: 'gid://gitlab/WorkItem/1',
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create confidential task',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: true,
},
- confidential: true,
- },
+ });
});
});
- // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('selects and add child', async () => {
- findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]);
+ describe('adding an existing work item', () => {
+ beforeEach(async () => {
+ await createComponent({ formType: FORM_TYPES.add });
+ });
- findAddChildButton().vm.$emit('click');
- await waitForPromises();
- expect(updateMutationResolver).toHaveBeenCalled();
- });
+ it('renders add form', () => {
+ expect(findForm().exists()).toBe(true);
+ expect(findTokenSelector().exists()).toBe(true);
+ expect(findAddChildButton().text()).toBe('Add task');
+ expect(findInput().exists()).toBe(false);
+ });
- // eslint-disable-next-line jest/no-disabled-tests
- describe.skip('when typing in combobox', () => {
- beforeEach(async () => {
- findCombobox().vm.$emit('input', 'Task');
+ it('searches for available work items as prop when typing in input', async () => {
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'Task');
await waitForPromises();
- await jest.runOnlyPendingTimers();
+
+ expect(availableWorkItemsResolver).toHaveBeenCalled();
});
- it('passes available work items as prop', () => {
- expect(findCombobox().exists()).toBe(true);
- expect(findCombobox().props('tokenList').length).toBe(2);
+ it('selects and adds children', async () => {
+ findTokenSelector().vm.$emit(
+ 'input',
+ availableWorkItemsResponse.data.workspace.workItems.nodes,
+ );
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ await waitForPromises();
+
+ expect(findAddChildButton().text()).toBe('Add tasks');
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(updateMutationResolver).toHaveBeenCalled();
});
+ });
+
+ describe('associate iteration with task', () => {
+ it('does not update iteration when mvc2 feature flag is not enabled', async () => {
+ await createComponent({
+ hasIterationsFeature: true,
+ parentIteration: mockParentIteration,
+ });
- it('passes action to create task', () => {
- expect(findCombobox().props('actionList').length).toBe(1);
+ 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,
+ });
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(updateMutationResolver).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ iterationWidget: {
+ iterationId: mockParentIteration.id,
+ },
+ },
+ });
+ });
+ it('does not update when parent has no iteration associated', async () => {
+ await createComponent({
+ workItemsMvc2Enabled: true,
+ hasIterationsFeature: true,
+ });
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(updateMutationResolver).not.toHaveBeenCalled();
});
});
});
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 6961996f912..66ce2c1becf 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
@@ -8,6 +8,7 @@ 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 { 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';
@@ -41,6 +42,13 @@ const issueDetailsResponse = (confidential = false) => ({
},
__typename: 'Iteration',
},
+ milestone: {
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/28',
+ title: 'v2.0',
+ __typename: 'Milestone',
+ },
__typename: 'Issue',
},
__typename: 'Project',
@@ -107,7 +115,9 @@ describe('WorkItemLinks', () => {
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findLinksBody = () => wrapper.findByTestId('links-body');
const findEmptyState = () => wrapper.findByTestId('links-empty');
+ const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
+ const findToggleCreateFormButton = () => wrapper.findByTestId('toggle-create-form');
const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
@@ -136,11 +146,27 @@ describe('WorkItemLinks', () => {
});
describe('add link form', () => {
- it('displays form on click add button and hides form on cancel', async () => {
+ it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
+ findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
expect(findAddLinksForm().exists()).toBe(true);
+ expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.add);
+
+ findAddLinksForm().vm.$emit('cancel');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(false);
+ });
+
+ it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => {
+ findToggleFormDropdown().vm.$emit('click');
+ findToggleCreateFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(true);
+ expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.create);
findAddLinksForm().vm.$emit('cancel');
await nextTick();
@@ -193,7 +219,7 @@ describe('WorkItemLinks', () => {
});
it('does not display button to toggle Add form', () => {
- expect(findToggleAddFormButton().exists()).toBe(false);
+ expect(findToggleFormDropdown().exists()).toBe(false);
});
it('does not display link menu on children', () => {
@@ -283,6 +309,7 @@ describe('WorkItemLinks', () => {
await createComponent({
issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
});
+ findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
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 08cdf62ae52..60ba2b55f76 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -9,7 +9,7 @@ import {
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
-import { resolvers, temporaryConfig } from '~/graphql_shared/issuable_client';
+import { resolvers, config } from '~/graphql_shared/issuable_client';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -22,8 +22,14 @@ import {
mockMilestoneWidgetResponse,
workItemResponseFactory,
updateWorkItemMutationErrorResponse,
+ workItemMilestoneSubscriptionResponse,
+ projectWorkItemResponse,
+ updateWorkItemMutationResponse,
} from 'jest/work_items/mock_data';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
describe('WorkItemMilestone component', () => {
Vue.use(VueApollo);
@@ -47,6 +53,8 @@ describe('WorkItemMilestone component', () => {
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+ const workItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const networkResolvedValue = new Error();
@@ -54,6 +62,12 @@ describe('WorkItemMilestone component', () => {
const successSearchWithNoMatchingMilestones = jest
.fn()
.mockResolvedValue(projectMilestonesResponseWithNoMilestones);
+ const milestoneSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemMilestoneSubscriptionResponse);
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
const showDropdown = () => {
findDropdown().vm.$emit('shown');
@@ -67,12 +81,20 @@ describe('WorkItemMilestone component', () => {
canUpdate = true,
milestone = mockMilestoneWidgetResponse,
searchQueryHandler = successSearchQueryHandler,
+ fetchByIid = false,
+ mutationHandler = successUpdateWorkItemMutationHandler,
} = {}) => {
const apolloProvider = createMockApollo(
- [[projectMilestonesQuery, searchQueryHandler]],
+ [
+ [workItemQuery, workItemQueryHandler],
+ [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [projectMilestonesQuery, searchQueryHandler],
+ [updateWorkItemMutation, mutationHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
+ ],
resolvers,
{
- typePolicies: temporaryConfig.cacheConfig.typePolicies,
+ typePolicies: config.cacheConfig.typePolicies,
},
);
@@ -92,6 +114,10 @@ describe('WorkItemMilestone component', () => {
workItemId,
workItemType,
fullPath,
+ queryVariables: {
+ id: workItemId,
+ },
+ fetchByIid,
},
stubs: {
GlDropdown,
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index ed90b11222a..635a1f326f8 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -41,6 +41,7 @@ export const workItemQueryResponse = {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Test',
state: 'OPEN',
description: 'description',
@@ -113,6 +114,7 @@ export const updateWorkItemMutationResponse = {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -178,6 +180,19 @@ export const mockParent = {
},
};
+export const descriptionTextWithCheckboxes = `- [ ] todo 1\n- [ ] todo 2`;
+
+export const descriptionHtmlWithCheckboxes = `
+ <ul dir="auto" class="task-list" data-sourcepos"1:1-2:12">
+ <li class="task-list-item" data-sourcepos="1:1-1:11">
+ <input class="task-list-item-checkbox" type="checkbox"> todo 1
+ </li>
+ <li class="task-list-item" data-sourcepos="2:1-2:12">
+ <input class="task-list-item-checkbox" type="checkbox"> todo 2
+ </li>
+ </ul>
+`;
+
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
@@ -193,12 +208,14 @@ export const workItemResponseFactory = ({
allowsScopedLabels = false,
lastEditedAt = null,
lastEditedBy = null,
+ withCheckboxes = false,
parent = mockParent.parent,
} = {}) => ({
data: {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: 1,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -224,9 +241,10 @@ export const workItemResponseFactory = ({
{
__typename: 'WorkItemWidgetDescription',
type: 'DESCRIPTION',
- description: 'some **great** text',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ description: withCheckboxes ? descriptionTextWithCheckboxes : 'some **great** text',
+ descriptionHtml: withCheckboxes
+ ? descriptionHtmlWithCheckboxes
+ : '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
lastEditedAt,
lastEditedBy,
},
@@ -283,11 +301,12 @@ export const workItemResponseFactory = ({
milestoneWidgetPresent
? {
__typename: 'WorkItemWidgetMilestone',
- dueDate: null,
- expired: false,
- id: 'gid://gitlab/Milestone/30',
- title: 'v4.0',
type: 'MILESTONE',
+ milestone: {
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ },
}
: { type: 'MOCK TYPE' },
{
@@ -312,7 +331,8 @@ export const workItemResponseFactory = ({
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
- id: 'gid://gitlab/WorkItem/1',
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
workItemTypes: {
nodes: [
{ id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
@@ -331,6 +351,7 @@ export const createWorkItemMutationResponse = {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -368,6 +389,7 @@ export const createWorkItemFromTaskMutationResponse = {
__typename: 'WorkItem',
description: 'New description',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
confidential: false,
@@ -405,6 +427,7 @@ export const createWorkItemFromTaskMutationResponse = {
newWorkItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1000000',
+ iid: '100',
title: 'Updated title',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
@@ -498,6 +521,28 @@ export const workItemTitleSubscriptionResponse = {
},
};
+export const workItemDescriptionSubscriptionResponse = {
+ data: {
+ issuableDescriptionUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetDescription',
+ type: 'DESCRIPTION',
+ description: 'New description',
+ descriptionHtml: '<p>New description</p>',
+ lastEditedAt: '2022-09-21T06:18:42Z',
+ lastEditedBy: {
+ id: 'gid://gitlab/User/2',
+ name: 'Someone else',
+ webPath: '/not-you',
+ },
+ },
+ ],
+ },
+ },
+};
+
export const workItemWeightSubscriptionResponse = {
data: {
issuableWeightUpdated: {
@@ -567,6 +612,25 @@ export const workItemIterationSubscriptionResponse = {
},
};
+export const workItemMilestoneSubscriptionResponse = {
+ data: {
+ issuableMilestoneUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ type: 'MILESTONE',
+ milestone: {
+ id: 'gid://gitlab/Milestone/1125',
+ expired: false,
+ title: 'Milestone title',
+ },
+ },
+ ],
+ },
+ },
+};
+
export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
@@ -776,6 +840,7 @@ export const changeWorkItemParentMutationResponse = {
},
description: null,
id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
state: 'OPEN',
title: 'Foo',
confidential: false,
@@ -809,22 +874,20 @@ export const availableWorkItemsResponse = {
__typename: 'Project',
id: 'gid://gitlab/Project/2',
workItems: {
- edges: [
+ nodes: [
{
- node: {
- id: 'gid://gitlab/WorkItem/458',
- title: 'Task 1',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- },
+ id: 'gid://gitlab/WorkItem/458',
+ title: 'Task 1',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ __typename: 'WorkItem',
},
{
- node: {
- id: 'gid://gitlab/WorkItem/459',
- title: 'Task 2',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- },
+ id: 'gid://gitlab/WorkItem/459',
+ title: 'Task 2',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ __typename: 'WorkItem',
},
],
},
@@ -1072,7 +1135,7 @@ export const groupIterationsResponseWithNoIterations = {
};
export const mockMilestoneWidgetResponse = {
- dueDate: null,
+ state: 'active',
expired: false,
id: 'gid://gitlab/Milestone/30',
title: 'v4.0',
@@ -1122,3 +1185,14 @@ export const projectMilestonesResponseWithNoMilestones = {
},
},
};
+
+export const projectWorkItemResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItemQueryResponse.data.workItem],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index 15dac25b7d9..387c8a355fa 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -37,12 +37,17 @@ describe('Create work item component', () => {
props = {},
queryHandler = querySuccessHandler,
mutationHandler = createWorkItemSuccessHandler,
+ fetchByIid = false,
} = {}) => {
- fakeApollo = createMockApollo([
- [projectWorkItemTypesQuery, queryHandler],
- [createWorkItemMutation, mutationHandler],
- [createWorkItemFromTaskMutation, mutationHandler],
- ]);
+ fakeApollo = createMockApollo(
+ [
+ [projectWorkItemTypesQuery, queryHandler],
+ [createWorkItemMutation, mutationHandler],
+ [createWorkItemFromTaskMutation, mutationHandler],
+ ],
+ {},
+ { typePolicies: { Project: { merge: true } } },
+ );
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
@@ -61,6 +66,9 @@ describe('Create work item component', () => {
},
provide: {
fullPath: 'full-path',
+ glFeatures: {
+ useIidInWorkItemsPath: fetchByIid,
+ },
},
});
};
@@ -99,7 +107,12 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
- expect(wrapper.vm.$router.push).toHaveBeenCalled();
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ name: 'workItem',
+ params: {
+ id: '1',
+ },
+ });
});
it('adds right margin for create button', () => {
@@ -197,4 +210,18 @@ describe('Create work item component', () => {
'Something went wrong when creating work item. Please try again.',
);
});
+
+ it('performs a correct redirect when `useIidInWorkItemsPath` feature flag is enabled', async () => {
+ createComponent({ fetchByIid: true });
+ findTitleInput().vm.$emit('title-input', 'Test title');
+
+ wrapper.find('form').trigger('submit');
+ await waitForPromises();
+
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ name: 'workItem',
+ params: { id: '1' },
+ query: { iid_path: 'true' },
+ });
+ });
});
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 d9372f2bcf0..880c4271024 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -55,6 +55,7 @@ describe('Work items root component', () => {
isModal: false,
workItemId: 'gid://gitlab/WorkItem/1',
workItemParentId: null,
+ iid: '1',
});
});
@@ -65,11 +66,15 @@ describe('Work items root component', () => {
deleteWorkItemHandler,
});
- findWorkItemDetail().vm.$emit('deleteWorkItem');
+ findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' });
await waitForPromises();
- expect(deleteWorkItemHandler).toHaveBeenCalled();
+ expect(deleteWorkItemHandler).toHaveBeenCalledWith({
+ input: {
+ id: '1',
+ },
+ });
expect(mockToastShow).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
});
@@ -81,7 +86,7 @@ describe('Work items root component', () => {
deleteWorkItemHandler,
});
- findWorkItemDetail().vm.$emit('deleteWorkItem');
+ findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' });
await waitForPromises();
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 66a917d8052..982f9f71f9e 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -10,6 +10,8 @@ import {
workItemTitleSubscriptionResponse,
workItemWeightSubscriptionResponse,
workItemLabelsSubscriptionResponse,
+ workItemMilestoneSubscriptionResponse,
+ workItemDescriptionSubscriptionResponse,
} from 'jest/work_items/mock_data';
import App from '~/work_items/components/app.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -17,6 +19,8 @@ import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subs
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
+import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
+import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
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';
@@ -34,6 +38,12 @@ describe('Work items router', () => {
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
const labelsSubscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
+ const milestoneSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemMilestoneSubscriptionResponse);
+ const descriptionSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemDescriptionSubscriptionResponse);
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
@@ -47,6 +57,8 @@ describe('Work items router', () => {
[workItemTitleSubscription, titleSubscriptionHandler],
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
[workItemLabelsSubscription, labelsSubscriptionHandler],
+ [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [workItemDescriptionSubscription, descriptionSubscriptionHandler],
];
if (IS_EE) {
@@ -59,10 +71,19 @@ describe('Work items router', () => {
provide: {
fullPath: 'full-path',
issuesListPath: 'full-path/-/issues',
+ hasIssueWeightsFeature: false,
},
});
};
+ beforeEach(() => {
+ window.gon = {
+ features: {
+ workItemsMvc2: false,
+ },
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
window.location.hash = '';
@@ -74,7 +95,14 @@ describe('Work items router', () => {
expect(wrapper.findComponent(WorkItemsRoot).exists()).toBe(true);
});
+ it('does not render create work item page on `/new` route if `workItemsMvc2` feature flag is off', async () => {
+ await createComponent('/new');
+
+ expect(wrapper.findComponent(CreateWorkItem).exists()).toBe(false);
+ });
+
it('renders create work item page on `/new` route', async () => {
+ window.gon.features.workItemsMvc2 = true;
await createComponent('/new');
expect(wrapper.findComponent(CreateWorkItem).exists()).toBe(true);