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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 14:10:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 14:10:13 +0300
commit0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch)
tree7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /spec/frontend
parent72123183a20411a36d607d70b12d57c484394c8e (diff)
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/dl_locator_helper.js28
-rw-r--r--spec/frontend/__helpers__/emoji.js10
-rw-r--r--spec/frontend/__helpers__/init_vue_mr_page_helper.js18
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_sprite_icon.js2
-rw-r--r--spec/frontend/access_tokens/components/access_token_table_app_spec.js241
-rw-r--r--spec/frontend/access_tokens/components/expires_at_field_spec.js33
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js169
-rw-r--r--spec/frontend/access_tokens/index_spec.js214
-rw-r--r--spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js148
-rw-r--r--spec/frontend/admin/users/index_spec.js8
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_counts_spec.js4
-rw-r--r--spec/frontend/api_spec.js29
-rw-r--r--spec/frontend/authentication/two_factor_auth/index_spec.js4
-rw-r--r--spec/frontend/awards_handler_spec.js29
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js69
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js10
-rw-r--r--spec/frontend/behaviors/markdown/render_mermaid_spec.js25
-rw-r--r--spec/frontend/blob/blob_file_dropzone_spec.js49
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap4
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js8
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js4
-rw-r--r--spec/frontend/blob/csv/csv_viewer_spec.js10
-rw-r--r--spec/frontend/blob/viewer/index_spec.js6
-rw-r--r--spec/frontend/boards/components/board_column_spec.js5
-rw-r--r--spec/frontend/boards/components/board_form_spec.js38
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js36
-rw-r--r--spec/frontend/boards/mock_data.js36
-rw-r--r--spec/frontend/boards/stores/actions_spec.js38
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js26
-rw-r--r--spec/frontend/cascading_settings/components/lock_popovers_spec.js10
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js)4
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js)9
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js)4
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_variable_table_spec.js)4
-rw-r--r--spec/frontend/clusters/agents/components/create_token_button_spec.js7
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap191
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js22
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js10
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js19
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js7
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js12
-rw-r--r--spec/frontend/confirm_modal_spec.js6
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap16
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_spec.js157
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js54
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js29
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js85
-rw-r--r--spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js30
-rw-r--r--spec/frontend/content_editor/extensions/footnote_definition_spec.js7
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js885
-rw-r--r--spec/frontend/content_editor/services/asset_resolver_spec.js10
-rw-r--r--spec/frontend/content_editor/services/code_block_language_loader_spec.js15
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js86
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_spec.js2
-rw-r--r--spec/frontend/cycle_analytics/path_navigation_spec.js6
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js23
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js6
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js1
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js1
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap4
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap2
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap2
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap23
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js21
-rw-r--r--spec/frontend/design_management/pages/index_spec.js20
-rw-r--r--spec/frontend/design_management/router_spec.js2
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js11
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js11
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js10
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js16
-rw-r--r--spec/frontend/diffs/store/utils_spec.js7
-rw-r--r--spec/frontend/editor/helpers.js6
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js1
-rw-r--r--spec/frontend/editor/source_editor_extension_spec.js1
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js67
-rw-r--r--spec/frontend/editor/source_editor_spec.js29
-rw-r--r--spec/frontend/editor/source_editor_webide_ext_spec.js55
-rw-r--r--spec/frontend/emoji/index_spec.js115
-rw-r--r--spec/frontend/emoji/utils_spec.js15
-rw-r--r--spec/frontend/environments/deploy_board_wrapper_spec.js4
-rw-r--r--spec/frontend/environments/environment_folder_spec.js4
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js4
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js2
-rw-r--r--spec/frontend/fixtures/integrations.rb (renamed from spec/frontend/fixtures/services.rb)6
-rw-r--r--spec/frontend/fixtures/prometheus_integration.rb (renamed from spec/frontend/fixtures/prometheus_service.rb)6
-rw-r--r--spec/frontend/fixtures/runner.rb6
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js2
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js24
-rw-r--r--spec/frontend/groups/components/app_spec.js68
-rw-r--r--spec/frontend/groups/components/empty_state_spec.js78
-rw-r--r--spec/frontend/groups/components/group_name_and_path_spec.js347
-rw-r--r--spec/frontend/groups/components/item_caret_spec.js4
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js7
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js210
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap2
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js6
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js240
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js8
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js15
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap4
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js1
-rw-r--r--spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js31
-rw-r--r--spec/frontend/integrations/edit/components/sections/configuration_spec.js57
-rw-r--r--spec/frontend/integrations/edit/components/sections/trigger_spec.js38
-rw-r--r--spec/frontend/integrations/edit/components/trigger_field_spec.js71
-rw-r--r--spec/frontend/integrations/edit/mock_data.js5
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js20
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js24
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js34
-rw-r--r--spec/frontend/issuable/components/csv_export_modal_spec.js32
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js4
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js81
-rw-r--r--spec/frontend/issuable/popover/components/mr_popover_spec.js119
-rw-r--r--spec/frontend/issuable/popover/index_spec.js (renamed from spec/frontend/mr_popover/index_spec.js)19
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js4
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js120
-rw-r--r--spec/frontend/issues/list/utils_spec.js24
-rw-r--r--spec/frontend/issues/show/components/description_spec.js5
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js24
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js72
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js87
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js87
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js105
-rw-r--r--spec/frontend/issues/show/components/incidents/utils_spec.js31
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js11
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/user_link_spec.js45
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js8
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js8
-rw-r--r--spec/frontend/labels/delete_label_modal_spec.js6
-rw-r--r--spec/frontend/lazy_loader_spec.js4
-rw-r--r--spec/frontend/lib/gfm/index_spec.js90
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js38
-rw-r--r--spec/frontend/lib/utils/forms_spec.js4
-rw-r--r--spec/frontend/lib/utils/rails_ujs_spec.js78
-rw-r--r--spec/frontend/lib/utils/table_utility_spec.js7
-rw-r--r--spec/frontend/lib/utils/users_cache_spec.js27
-rw-r--r--spec/frontend/logs/utils_spec.js38
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js14
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js31
-rw-r--r--spec/frontend/members/index_spec.js2
-rw-r--r--spec/frontend/members/utils_spec.js2
-rw-r--r--spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js2
-rw-r--r--spec/frontend/merge_request_tabs_spec.js22
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js18
-rw-r--r--spec/frontend/monitoring/fixture_data.js3
-rw-r--r--spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap91
-rw-r--r--spec/frontend/mr_popover/mr_popover_spec.js80
-rw-r--r--spec/frontend/nav/components/responsive_header_spec.js4
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js4
-rw-r--r--spec/frontend/notes/components/comment_field_layout_spec.js4
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js32
-rw-r--r--spec/frontend/notes/components/note_body_spec.js35
-rw-r--r--spec/frontend/notes/components/note_header_spec.js16
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js19
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js4
-rw-r--r--spec/frontend/notes/mock_data.js10
-rw-r--r--spec/frontend/notes/stores/actions_spec.js34
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js12
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js12
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js52
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js41
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js12
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap (renamed from spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap)12
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js (renamed from spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js167
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js160
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap4
-rw-r--r--spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js4
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js2
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js109
-rw-r--r--spec/frontend/performance_bar/components/add_request_spec.js28
-rw-r--r--spec/frontend/performance_bar/index_spec.js12
-rw-r--r--spec/frontend/performance_bar/services/performance_bar_service_spec.js12
-rw-r--r--spec/frontend/performance_bar/stores/performance_bar_store_spec.js9
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js27
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js48
-rw-r--r--spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js33
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js40
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js93
-rw-r--r--spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js40
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js118
-rw-r--r--spec/frontend/pipeline_wizard/components/input_wrapper_spec.js (renamed from spec/frontend/pipeline_wizard/components/input_spec.js)2
-rw-r--r--spec/frontend/pipeline_wizard/components/step_spec.js2
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js46
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js37
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js323
-rw-r--r--spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js146
-rw-r--r--spec/frontend/pipelines/pipeline_tabs_spec.js95
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js21
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js13
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js2
-rw-r--r--spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js45
-rw-r--r--spec/frontend/projects/compare/components/revision_card_spec.js8
-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.js30
-rw-r--r--spec/frontend/projects/project_new_spec.js92
-rw-r--r--spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js101
-rw-r--r--spec/frontend/projects/settings/branch_rules/rule_edit_spec.js49
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js18
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js4
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js2
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js16
-rw-r--r--spec/frontend/repository/components/blob_viewers/sketch_viewer_spec.js32
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js2
-rw-r--r--spec/frontend/repository/components/table/index_spec.js4
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js64
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js26
-rw-r--r--spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap2
-rw-r--r--spec/frontend/runner/components/cells/runner_status_cell_spec.js17
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js16
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js1
-rw-r--r--spec/frontend/runner/components/runner_details_spec.js70
-rw-r--r--spec/frontend/runner/components/runner_jobs_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_list_empty_state_spec.js76
-rw-r--r--spec/frontend/runner/components/runner_projects_spec.js4
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js10
-rw-r--r--spec/frontend/runner/mock_data.js5
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js20
-rw-r--r--spec/frontend/search/store/actions_spec.js19
-rw-r--r--spec/frontend/search_autocomplete_spec.js2
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js94
-rw-r--r--spec/frontend/security_configuration/mock_data.js9
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js24
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js2
-rw-r--r--spec/frontend/sidebar/components/attention_requested_toggle_spec.js12
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js4
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js2
-rw-r--r--spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js7
-rw-r--r--spec/frontend/sidebar/components/time_tracking/mock_data.js28
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js86
-rw-r--r--spec/frontend/static_site_editor/components/app_spec.js34
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js264
-rw-r--r--spec/frontend/static_site_editor/components/edit_drawer_spec.js67
-rw-r--r--spec/frontend/static_site_editor/components/edit_header_spec.js38
-rw-r--r--spec/frontend/static_site_editor/components/edit_meta_controls_spec.js115
-rw-r--r--spec/frontend/static_site_editor/components/edit_meta_modal_spec.js172
-rw-r--r--spec/frontend/static_site_editor/components/front_matter_controls_spec.js71
-rw-r--r--spec/frontend/static_site_editor/components/invalid_content_message_spec.js23
-rw-r--r--spec/frontend/static_site_editor/components/publish_toolbar_spec.js92
-rw-r--r--spec/frontend/static_site_editor/components/submit_changes_error_spec.js48
-rw-r--r--spec/frontend/static_site_editor/components/unsaved_changes_confirm_dialog_spec.js44
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/file_spec.js25
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/has_submitted_changes_spec.js27
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js37
-rw-r--r--spec/frontend/static_site_editor/mock_data.js91
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js301
-rw-r--r--spec/frontend/static_site_editor/pages/success_spec.js131
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/editor_service_spec.js214
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js77
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab_spec.js41
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/insert_video_modal_spec.js44
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_integration_spec.js69
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_spec.js222
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/build_custom_renderer_spec.js32
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer_spec.js218
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token_spec.js88
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/mock_data.js54
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition_spec.js25
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_spec.js24
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js33
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_heading_spec.js12
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_html_block_spec.js37
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js55
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js84
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_list_item_spec.js12
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_softbreak_spec.js23
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_utils_spec.js109
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/sanitize_html_spec.js11
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/toolbar_item_spec.js57
-rw-r--r--spec/frontend/static_site_editor/services/formatter_spec.js39
-rw-r--r--spec/frontend/static_site_editor/services/front_matterify_spec.js54
-rw-r--r--spec/frontend/static_site_editor/services/generate_branch_name_spec.js22
-rw-r--r--spec/frontend/static_site_editor/services/load_source_content_spec.js36
-rw-r--r--spec/frontend/static_site_editor/services/parse_source_file_spec.js101
-rw-r--r--spec/frontend/static_site_editor/services/renderers/render_image_spec.js96
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js261
-rw-r--r--spec/frontend/static_site_editor/services/templater_spec.js112
-rw-r--r--spec/frontend/tags/components/delete_tag_modal_spec.js138
-rw-r--r--spec/frontend/tags/init_delete_tag_modal_spec.js23
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js1
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js41
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js4
-rw-r--r--spec/frontend/user_popovers_spec.js137
-rw-r--r--spec/frontend/users_select/test_helper.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js50
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js18
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/index_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js176
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js42
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js17
-rw-r--r--spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js24
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js236
-rw-r--r--spec/frontend/vue_mr_widget/test_extensions.js51
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap120
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js192
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js113
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js40
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js30
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/papa_parse_alert_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js234
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap30
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js35
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js9
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js3
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js15
-rw-r--r--spec/frontend/vue_shared/issuable/show/mock_data.js1
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js93
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js222
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js12
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js88
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js37
-rw-r--r--spec/frontend/work_items/components/work_item_weight_spec.js47
-rw-r--r--spec/frontend/work_items/mock_data.js151
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js105
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js2
367 files changed, 9315 insertions, 7085 deletions
diff --git a/spec/frontend/__helpers__/dl_locator_helper.js b/spec/frontend/__helpers__/dl_locator_helper.js
new file mode 100644
index 00000000000..b507dcd599d
--- /dev/null
+++ b/spec/frontend/__helpers__/dl_locator_helper.js
@@ -0,0 +1,28 @@
+import { createWrapper, ErrorWrapper } from '@vue/test-utils';
+
+/**
+ * Find the definition (<dd>) that corresponds to this term (<dt>)
+ *
+ * Given html in the `wrapper`:
+ *
+ * <dl>
+ * <dt>My label</dt>
+ * <dd>Value</dd>
+ * </dl>
+ *
+ * findDd('My label', wrapper)
+ *
+ * Returns `<dd>Value</dd>`
+ *
+ * @param {object} wrapper - Parent wrapper
+ * @param {string} dtLabel - Label for this value
+ * @returns Wrapper
+ */
+export const findDd = (dtLabel, wrapper) => {
+ const dt = wrapper.findByText(dtLabel).element;
+ const dd = dt.nextElementSibling;
+ if (dt.tagName === 'DT' && dd.tagName === 'DD') {
+ return createWrapper(dd, {});
+ }
+ return ErrorWrapper(dtLabel);
+};
diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js
index 014a7854024..6c9291bdc8f 100644
--- a/spec/frontend/__helpers__/emoji.js
+++ b/spec/frontend/__helpers__/emoji.js
@@ -58,6 +58,16 @@ export const validEmoji = {
unicodeVersion: '6.0',
description: 'because it contains multiple zero width joiners',
},
+ thumbsup: {
+ moji: '👍',
+ unicodeVersion: '6.0',
+ description: 'thumbs up sign',
+ },
+ thumbsdown: {
+ moji: '👎',
+ description: 'thumbs down sign',
+ unicodeVersion: '6.0',
+ },
};
export const invalidEmoji = {
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index ee01e9e6268..6b719a32480 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -13,16 +13,16 @@ export default function initVueMRPage() {
const diffsAppProjectPath = 'testproject';
const mrEl = document.createElement('div');
mrEl.className = 'merge-request fixture-mr';
- mrEl.setAttribute('data-mr-action', 'diffs');
+ mrEl.dataset.mrAction = 'diffs';
mrTestEl.appendChild(mrEl);
const mrDiscussionsEl = document.createElement('div');
mrDiscussionsEl.id = 'js-vue-mr-discussions';
- mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
- mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
- mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
- mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
- mrDiscussionsEl.setAttribute('data-is-locked', 'false');
+ mrDiscussionsEl.dataset.currentUserData = JSON.stringify(userDataMock);
+ mrDiscussionsEl.dataset.noteableData = JSON.stringify(noteableDataMock);
+ mrDiscussionsEl.dataset.notesData = JSON.stringify(notesDataMock);
+ mrDiscussionsEl.dataset.noteableType = 'merge-request';
+ mrDiscussionsEl.dataset.isLocked = 'false';
mrTestEl.appendChild(mrDiscussionsEl);
const discussionCounterEl = document.createElement('div');
@@ -31,9 +31,9 @@ export default function initVueMRPage() {
const diffsAppEl = document.createElement('div');
diffsAppEl.id = 'js-diffs-app';
- diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
- diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
- diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
+ diffsAppEl.dataset.endpoint = diffsAppEndpoint;
+ diffsAppEl.dataset.projectPath = diffsAppProjectPath;
+ diffsAppEl.dataset.currentUserData = JSON.stringify(userDataMock);
mrTestEl.appendChild(diffsAppEl);
const mock = new MockAdapter(axios);
diff --git a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
index bce9d93bea8..45b9c31c4db 100644
--- a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
+++ b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
@@ -9,7 +9,7 @@ export const toHaveSpriteIcon = (element, iconName) => {
const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
const matchingIcon = iconReferences.find(
- (reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
+ (reference) => reference.parentNode.dataset.testid === `${iconName}-icon`,
);
const pass = Boolean(matchingIcon);
diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
new file mode 100644
index 00000000000..b45abe418e4
--- /dev/null
+++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
@@ -0,0 +1,241 @@
+import { GlPagination, GlTable } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
+import { EVENT_SUCCESS, PAGE_SIZE } from '~/access_tokens/components/constants';
+import { __, s__, sprintf } from '~/locale';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+
+describe('~/access_tokens/components/access_token_table_app', () => {
+ let wrapper;
+
+ const accessTokenType = 'personal access token';
+ const accessTokenTypePlural = 'personal access tokens';
+ const initialActiveAccessTokens = [];
+ const noActiveTokensMessage = 'This user has no active personal access tokens.';
+ const showRole = false;
+
+ const defaultActiveAccessTokens = [
+ {
+ name: 'a',
+ scopes: ['api'],
+ created_at: '2021-05-01T00:00:00.000Z',
+ last_used_at: null,
+ expired: false,
+ expires_soon: true,
+ expires_at: null,
+ revoked: false,
+ revoke_path: '/-/profile/personal_access_tokens/1/revoke',
+ role: 'Maintainer',
+ },
+ {
+ name: 'b',
+ scopes: ['api', 'sudo'],
+ created_at: '2022-04-21T00:00:00.000Z',
+ last_used_at: '2022-04-21T00:00:00.000Z',
+ expired: true,
+ expires_soon: false,
+ expires_at: new Date().toISOString(),
+ revoked: false,
+ revoke_path: '/-/profile/personal_access_tokens/2/revoke',
+ role: 'Maintainer',
+ },
+ ];
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(AccessTokenTableApp, {
+ provide: {
+ accessTokenType,
+ accessTokenTypePlural,
+ initialActiveAccessTokens,
+ noActiveTokensMessage,
+ showRole,
+ ...props,
+ },
+ });
+ };
+
+ const triggerSuccess = async (activeAccessTokens = defaultActiveAccessTokens) => {
+ wrapper
+ .findComponent(DomElementListener)
+ .vm.$emit(EVENT_SUCCESS, { detail: [{ active_access_tokens: activeAccessTokens }] });
+ await nextTick();
+ };
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findHeaders = () => findTable().findAll('th > :first-child');
+ const findCells = () => findTable().findAll('td');
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('should render the `GlTable` with default empty message', () => {
+ createComponent();
+
+ const cells = findCells();
+ expect(cells).toHaveLength(1);
+ expect(cells.at(0).text()).toBe(
+ sprintf(__('This user has no active %{accessTokenTypePlural}.'), { accessTokenTypePlural }),
+ );
+ });
+
+ it('should render the `GlTable` with custom empty message', () => {
+ const noTokensMessage = 'This group has no active access tokens.';
+ createComponent({ noActiveTokensMessage: noTokensMessage });
+
+ const cells = findCells();
+ expect(cells).toHaveLength(1);
+ expect(cells.at(0).text()).toBe(noTokensMessage);
+ });
+
+ it('should render an h5 element', () => {
+ createComponent();
+
+ expect(wrapper.find('h5').text()).toBe(
+ sprintf(__('Active %{accessTokenTypePlural} (%{totalAccessTokens})'), {
+ accessTokenTypePlural,
+ totalAccessTokens: initialActiveAccessTokens.length,
+ }),
+ );
+ });
+
+ it('should render the `GlTable` component with default 6 column headers', () => {
+ createComponent();
+
+ const headers = findHeaders();
+ expect(headers).toHaveLength(6);
+ [
+ __('Token name'),
+ __('Scopes'),
+ s__('AccessTokens|Created'),
+ __('Last Used'),
+ __('Expires'),
+ __('Action'),
+ ].forEach((text, index) => {
+ expect(headers.at(index).text()).toBe(text);
+ });
+ });
+
+ it('should render the `GlTable` component with 7 headers', () => {
+ createComponent({ showRole: true });
+
+ const headers = findHeaders();
+ expect(headers).toHaveLength(7);
+ [
+ __('Token name'),
+ __('Scopes'),
+ s__('AccessTokens|Created'),
+ __('Last Used'),
+ __('Expires'),
+ __('Role'),
+ __('Action'),
+ ].forEach((text, index) => {
+ expect(headers.at(index).text()).toBe(text);
+ });
+ });
+
+ it('`Last Used` header should contain a link and an assistive message', () => {
+ createComponent();
+
+ const headers = wrapper.findAll('th');
+ const lastUsed = headers.at(3);
+ const anchor = lastUsed.find('a');
+ const assistiveElement = lastUsed.find('.gl-sr-only');
+ expect(anchor.exists()).toBe(true);
+ expect(anchor.attributes('href')).toBe(
+ '/help/user/profile/personal_access_tokens.md#view-the-last-time-a-token-was-used',
+ );
+ expect(assistiveElement.text()).toBe(s__('AccessTokens|The last time a token was used'));
+ });
+
+ it('updates the table after a success AJAX event', async () => {
+ createComponent({ showRole: true });
+ await triggerSuccess();
+
+ const cells = findCells();
+ expect(cells).toHaveLength(14);
+
+ // First row
+ expect(cells.at(0).text()).toBe('a');
+ expect(cells.at(1).text()).toBe('api');
+ expect(cells.at(2).text()).not.toBe(__('Never'));
+ expect(cells.at(3).text()).toBe(__('Never'));
+ expect(cells.at(4).text()).toBe(__('Never'));
+ expect(cells.at(5).text()).toBe('Maintainer');
+ let anchor = cells.at(6).find('a');
+ expect(anchor.attributes()).toMatchObject({
+ 'aria-label': __('Revoke'),
+ 'data-qa-selector': __('revoke_button'),
+ href: '/-/profile/personal_access_tokens/1/revoke',
+ 'data-confirm': sprintf(
+ __(
+ 'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.',
+ ),
+ { accessTokenType },
+ ),
+ });
+
+ expect(anchor.classes()).toContain('btn-danger-secondary');
+
+ // Second row
+ expect(cells.at(7).text()).toBe('b');
+ expect(cells.at(8).text()).toBe('api, sudo');
+ expect(cells.at(9).text()).not.toBe(__('Never'));
+ expect(cells.at(10).text()).not.toBe(__('Never'));
+ expect(cells.at(11).text()).toBe(__('Expired'));
+ expect(cells.at(12).text()).toBe('Maintainer');
+ anchor = cells.at(13).find('a');
+ expect(anchor.attributes('href')).toBe('/-/profile/personal_access_tokens/2/revoke');
+ expect(anchor.classes()).toEqual(['btn', 'btn-danger', 'btn-md', 'gl-button', 'btn-icon']);
+ });
+
+ it('sorts rows alphabetically', async () => {
+ createComponent({ showRole: true });
+ await triggerSuccess();
+
+ const cells = findCells();
+
+ // First and second rows
+ expect(cells.at(0).text()).toBe('a');
+ expect(cells.at(7).text()).toBe('b');
+
+ const headers = findHeaders();
+ await headers.at(0).trigger('click');
+ await headers.at(0).trigger('click');
+
+ // First and second rows have swapped
+ expect(cells.at(0).text()).toBe('b');
+ expect(cells.at(7).text()).toBe('a');
+ });
+
+ it('sorts rows by date', async () => {
+ createComponent({ showRole: true });
+ await triggerSuccess();
+
+ const cells = findCells();
+
+ // First and second rows
+ expect(cells.at(3).text()).toBe('Never');
+ expect(cells.at(10).text()).not.toBe('Never');
+
+ const headers = findHeaders();
+ await headers.at(3).trigger('click');
+
+ // First and second rows have swapped
+ expect(cells.at(3).text()).not.toBe('Never');
+ expect(cells.at(10).text()).toBe('Never');
+ });
+
+ it('should show the pagination component when needed', async () => {
+ createComponent();
+ expect(findPagination().exists()).toBe(false);
+
+ await triggerSuccess(Array(PAGE_SIZE).fill(defaultActiveAccessTokens[0]));
+ expect(findPagination().exists()).toBe(false);
+
+ await triggerSuccess(Array(PAGE_SIZE + 1).fill(defaultActiveAccessTokens[0]));
+ expect(findPagination().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/access_tokens/components/expires_at_field_spec.js b/spec/frontend/access_tokens/components/expires_at_field_spec.js
index fc8edcb573f..cb899d10ba7 100644
--- a/spec/frontend/access_tokens/components/expires_at_field_spec.js
+++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlDatepicker } from '@gitlab/ui';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
describe('~/access_tokens/components/expires_at_field', () => {
@@ -12,22 +13,40 @@ describe('~/access_tokens/components/expires_at_field', () => {
},
};
- const createComponent = (propsData = defaultPropsData) => {
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
+
+ const createComponent = (props = {}) => {
wrapper = shallowMount(ExpiresAtField, {
- propsData,
+ propsData: {
+ ...defaultPropsData,
+ ...props,
+ },
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
it('should render datepicker with input info', () => {
+ createComponent();
+
expect(wrapper.element).toMatchSnapshot();
});
+
+ it('should set the date pickers minimum date', () => {
+ const minDate = new Date('1970-01-01');
+
+ createComponent({ minDate });
+
+ expect(findDatepicker().props('minDate')).toStrictEqual(minDate);
+ });
+
+ it('should set the date pickers maximum date', () => {
+ const maxDate = new Date('1970-01-01');
+
+ createComponent({ maxDate });
+
+ expect(findDatepicker().props('maxDate')).toStrictEqual(maxDate);
+ });
});
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
new file mode 100644
index 00000000000..9ccadbebf7a
--- /dev/null
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -0,0 +1,169 @@
+import { GlAlert } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
+import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from '~/access_tokens/components/constants';
+import { createAlert, VARIANT_INFO } from '~/flash';
+import { __, sprintf } from '~/locale';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
+
+jest.mock('~/flash');
+
+describe('~/access_tokens/components/new_access_token_app', () => {
+ let wrapper;
+
+ const accessTokenType = 'personal access token';
+
+ const createComponent = (provide = { accessTokenType }) => {
+ wrapper = mountExtended(NewAccessTokenApp, {
+ provide,
+ });
+ };
+
+ const triggerSuccess = async (newToken = 'new token') => {
+ wrapper.find(DomElementListener).vm.$emit(EVENT_SUCCESS, { detail: [{ new_token: newToken }] });
+ await nextTick();
+ };
+
+ const triggerError = async (errors = ['1', '2']) => {
+ wrapper.find(DomElementListener).vm.$emit(EVENT_ERROR, { detail: [{ errors }] });
+ await nextTick();
+ };
+
+ beforeEach(() => {
+ // NewAccessTokenApp observes a form element
+ setHTMLFixture(`<form id="${FORM_SELECTOR.slice(1)}"><input type="submit"/></form>`);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ wrapper.destroy();
+ createAlert.mockClear();
+ });
+
+ it('should render nothing', () => {
+ expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ });
+
+ describe('on success', () => {
+ it('should render `InputCopyToggleVisibility` component', async () => {
+ const newToken = '12345';
+ await triggerSuccess(newToken);
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+
+ const InputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility);
+ expect(InputCopyToggleVisibilityComponent.props('value')).toBe(newToken);
+ 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 }),
+ );
+ });
+
+ it('input field should contain QA-related selectors', async () => {
+ const newToken = '12345';
+ await triggerSuccess(newToken);
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+
+ const inputAttributes = wrapper
+ .findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType }))
+ .attributes();
+ expect(inputAttributes).toMatchObject({
+ class: expect.stringContaining('qa-created-access-token'),
+ 'data-qa-selector': 'created_access_token_field',
+ });
+ });
+
+ it('should render an info alert', async () => {
+ await triggerSuccess();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(__('Your new %{accessTokenType} has been created.'), {
+ accessTokenType,
+ }),
+ variant: VARIANT_INFO,
+ });
+ });
+
+ it('should reset the form', async () => {
+ const resetSpy = jest.spyOn(wrapper.vm.form, 'reset');
+
+ await triggerSuccess();
+
+ expect(resetSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('on error', () => {
+ it('should render an error alert', async () => {
+ await triggerError(['first', 'second']);
+
+ expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false);
+
+ let GlAlertComponent = wrapper.findComponent(GlAlert);
+ expect(GlAlertComponent.props('title')).toBe(__('The form contains the following errors:'));
+ expect(GlAlertComponent.props('variant')).toBe('danger');
+ let itemEls = wrapper.findAll('li');
+ expect(itemEls).toHaveLength(2);
+ expect(itemEls.at(0).text()).toBe('first');
+ expect(itemEls.at(1).text()).toBe('second');
+
+ await triggerError(['one']);
+
+ GlAlertComponent = wrapper.findComponent(GlAlert);
+ expect(GlAlertComponent.props('title')).toBe(__('The form contains the following error:'));
+ expect(GlAlertComponent.props('variant')).toBe('danger');
+ itemEls = wrapper.findAll('li');
+ expect(itemEls).toHaveLength(1);
+ });
+
+ it('the error alert should be dismissible', async () => {
+ await triggerError();
+
+ const GlAlertComponent = wrapper.findComponent(GlAlert);
+ expect(GlAlertComponent.exists()).toBe(true);
+
+ GlAlertComponent.vm.$emit('dismiss');
+ await nextTick();
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ });
+ });
+
+ describe('before error or success', () => {
+ it('should scroll to the container', async () => {
+ const containerEl = wrapper.vm.$refs.container;
+ const scrollIntoViewSpy = jest.spyOn(containerEl, 'scrollIntoView');
+
+ await triggerSuccess();
+
+ expect(scrollIntoViewSpy).toHaveBeenCalledWith(false);
+ expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1);
+
+ await triggerError();
+
+ expect(scrollIntoViewSpy).toHaveBeenCalledWith(false);
+ expect(scrollIntoViewSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('should dismiss the info alert', async () => {
+ const dismissSpy = jest.fn();
+ createAlert.mockReturnValue({ dismiss: dismissSpy });
+
+ await triggerSuccess();
+ await triggerError();
+
+ expect(dismissSpy).toHaveBeenCalled();
+ expect(dismissSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index 1d8ac7cec25..b6119f1d167 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -1,27 +1,118 @@
+/* eslint-disable vue/require-prop-types */
+/* eslint-disable vue/one-component-per-file */
import { createWrapper } from '@vue/test-utils';
import Vue from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { initExpiresAtField, initProjectsField } from '~/access_tokens';
+import {
+ initAccessTokenTableApp,
+ initExpiresAtField,
+ initNewAccessTokenApp,
+ initProjectsField,
+ initTokensApp,
+} from '~/access_tokens';
+import * as AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
import * as ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
+import * as NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
import * as ProjectsField from '~/access_tokens/components/projects_field.vue';
+import * as TokensApp from '~/access_tokens/components/tokens_app.vue';
+import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants';
+import { __, sprintf } from '~/locale';
describe('access tokens', () => {
- const FakeComponent = Vue.component('FakeComponent', {
- props: {
- inputAttrs: {
- type: Object,
- required: true,
- },
- },
- render: () => null,
- });
+ let wrapper;
- beforeEach(() => {
- window.gon = { features: { personalAccessTokensScopedToProjects: true } };
+ afterEach(() => {
+ wrapper?.destroy();
+ resetHTMLFixture();
});
- afterEach(() => {
- document.body.innerHTML = '';
+ describe('initAccessTokenTableApp', () => {
+ const accessTokenType = 'personal access token';
+ const accessTokenTypePlural = 'personal access tokens';
+ const initialActiveAccessTokens = [{ id: '1' }];
+
+ const FakeAccessTokenTableApp = Vue.component('FakeComponent', {
+ inject: [
+ 'accessTokenType',
+ 'accessTokenTypePlural',
+ 'initialActiveAccessTokens',
+ 'noActiveTokensMessage',
+ 'showRole',
+ ],
+ props: [
+ 'accessTokenType',
+ 'accessTokenTypePlural',
+ 'initialActiveAccessTokens',
+ 'noActiveTokensMessage',
+ 'showRole',
+ ],
+ render: () => null,
+ });
+ AccessTokenTableApp.default = FakeAccessTokenTableApp;
+
+ it('mounts the component and provides required values', () => {
+ setHTMLFixture(
+ `<div id="js-access-token-table-app"
+ data-access-token-type="${accessTokenType}"
+ data-access-token-type-plural="${accessTokenTypePlural}"
+ data-initial-active-access-tokens=${JSON.stringify(initialActiveAccessTokens)}
+ >
+ </div>`,
+ );
+
+ const vueInstance = initAccessTokenTableApp();
+
+ wrapper = createWrapper(vueInstance);
+ const component = wrapper.findComponent(FakeAccessTokenTableApp);
+
+ expect(component.exists()).toBe(true);
+
+ expect(component.props()).toMatchObject({
+ // Required value
+ accessTokenType,
+ accessTokenTypePlural,
+ initialActiveAccessTokens,
+
+ // Default values
+ noActiveTokensMessage: sprintf(__('This user has no active %{accessTokenTypePlural}.'), {
+ accessTokenTypePlural,
+ }),
+ showRole: false,
+ });
+ });
+
+ it('mounts the component and provides all values', () => {
+ const noActiveTokensMessage = 'This group has no active access tokens.';
+ setHTMLFixture(
+ `<div id="js-access-token-table-app"
+ data-access-token-type="${accessTokenType}"
+ data-access-token-type-plural="${accessTokenTypePlural}"
+ data-initial-active-access-tokens=${JSON.stringify(initialActiveAccessTokens)}
+ data-no-active-tokens-message="${noActiveTokensMessage}"
+ data-show-role
+ >
+ </div>`,
+ );
+
+ const vueInstance = initAccessTokenTableApp();
+
+ wrapper = createWrapper(vueInstance);
+ const component = wrapper.findComponent(FakeAccessTokenTableApp);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props()).toMatchObject({
+ accessTokenType,
+ accessTokenTypePlural,
+ initialActiveAccessTokens,
+ noActiveTokensMessage,
+ showRole: true,
+ });
+ });
+
+ it('returns `null`', () => {
+ expect(initNewAccessTokenApp()).toBe(null);
+ });
});
describe.each`
@@ -30,33 +121,42 @@ describe('access tokens', () => {
${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => {
describe('when mount element exists', () => {
+ const FakeComponent = Vue.component('FakeComponent', {
+ props: ['inputAttrs'],
+ render: () => null,
+ });
+
const nameAttribute = `access_tokens[${fieldName}]`;
const idAttribute = `access_tokens_${fieldName}`;
beforeEach(() => {
- const mountEl = document.createElement('div');
- mountEl.classList.add(mountSelector);
-
- const input = document.createElement('input');
- input.setAttribute('name', nameAttribute);
- input.setAttribute('data-js-name', fieldName);
- input.setAttribute('id', idAttribute);
- input.setAttribute('placeholder', 'Foo bar');
- input.setAttribute('value', '1,2');
+ window.gon = { features: { personalAccessTokensScopedToProjects: true } };
- mountEl.appendChild(input);
-
- document.body.appendChild(mountEl);
+ setHTMLFixture(
+ `<div class="${mountSelector}">
+ <input
+ name="${nameAttribute}"
+ data-js-name="${fieldName}"
+ id="${idAttribute}"
+ placeholder="Foo bar"
+ value="1,2"
+ />
+ </div>`,
+ );
// Mock component so we don't have to deal with mocking Apollo
// eslint-disable-next-line no-param-reassign
expectedComponent.default = FakeComponent;
});
+ afterEach(() => {
+ delete window.gon;
+ });
+
it('mounts component and sets `inputAttrs` prop', async () => {
const vueInstance = await initFunction();
- const wrapper = createWrapper(vueInstance);
+ wrapper = createWrapper(vueInstance);
const component = wrapper.findComponent(FakeComponent);
expect(component.exists()).toBe(true);
@@ -75,4 +175,64 @@ describe('access tokens', () => {
});
});
});
+
+ describe('initNewAccessTokenApp', () => {
+ it('mounts the component and sets `accessTokenType` prop', () => {
+ const accessTokenType = 'personal access token';
+ setHTMLFixture(
+ `<div id="js-new-access-token-app" data-access-token-type="${accessTokenType}"></div>`,
+ );
+
+ const FakeNewAccessTokenApp = Vue.component('FakeComponent', {
+ inject: ['accessTokenType'],
+ props: ['accessTokenType'],
+ render: () => null,
+ });
+ NewAccessTokenApp.default = FakeNewAccessTokenApp;
+
+ const vueInstance = initNewAccessTokenApp();
+
+ wrapper = createWrapper(vueInstance);
+ const component = wrapper.findComponent(FakeNewAccessTokenApp);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props('accessTokenType')).toEqual(accessTokenType);
+ });
+
+ it('returns `null`', () => {
+ expect(initNewAccessTokenApp()).toBe(null);
+ });
+ });
+
+ describe('initTokensApp', () => {
+ it('mounts the component and provides`tokenTypes` ', () => {
+ const tokensData = {
+ [FEED_TOKEN]: FEED_TOKEN,
+ [INCOMING_EMAIL_TOKEN]: INCOMING_EMAIL_TOKEN,
+ [STATIC_OBJECT_TOKEN]: STATIC_OBJECT_TOKEN,
+ };
+ setHTMLFixture(
+ `<div id="js-tokens-app" data-tokens-data=${JSON.stringify(tokensData)}></div>`,
+ );
+
+ const FakeTokensApp = Vue.component('FakeComponent', {
+ inject: ['tokenTypes'],
+ props: ['tokenTypes'],
+ render: () => null,
+ });
+ TokensApp.default = FakeTokensApp;
+
+ const vueInstance = initTokensApp();
+
+ wrapper = createWrapper(vueInstance);
+ const component = wrapper.findComponent(FakeTokensApp);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props('tokenTypes')).toEqual(tokensData);
+ });
+
+ it('returns `null`', () => {
+ expect(initNewAccessTokenApp()).toBe(null);
+ });
+ });
});
diff --git a/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
new file mode 100644
index 00000000000..2db997942a7
--- /dev/null
+++ b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
@@ -0,0 +1,148 @@
+import { GlFormCheckbox } from '@gitlab/ui';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import SettingsForm from '~/admin/application_settings/inactive_project_deletion/components/form.vue';
+
+describe('Form component', () => {
+ let wrapper;
+
+ const findEnabledCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findProjectDeletionSettings = () =>
+ wrapper.findByTestId('inactive-project-deletion-settings');
+ const findMinSizeGroup = () => wrapper.findByTestId('min-size-group');
+ const findMinSizeInputGroup = () => wrapper.findByTestId('min-size-input-group');
+ const findMinSizeInput = () => wrapper.findByTestId('min-size-input');
+ const findDeleteAfterMonthsGroup = () => wrapper.findByTestId('delete-after-months-group');
+ const findDeleteAfterMonthsInputGroup = () =>
+ wrapper.findByTestId('delete-after-months-input-group');
+ const findDeleteAfterMonthsInput = () => wrapper.findByTestId('delete-after-months-input');
+ const findSendWarningEmailAfterMonthsGroup = () =>
+ wrapper.findByTestId('send-warning-email-after-months-group');
+ const findSendWarningEmailAfterMonthsInputGroup = () =>
+ wrapper.findByTestId('send-warning-email-after-months-input-group');
+ const findSendWarningEmailAfterMonthsInput = () =>
+ wrapper.findByTestId('send-warning-email-after-months-input');
+
+ const createComponent = (
+ mountFn = shallowMountExtended,
+ propsData = { deleteInactiveProjects: true },
+ ) => {
+ wrapper = mountFn(SettingsForm, { propsData });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Enable inactive project deletion', () => {
+ it('has the checkbox', () => {
+ createComponent();
+
+ expect(findEnabledCheckbox().exists()).toBe(true);
+ });
+
+ it.each([[true], [false]])(
+ 'when the checkbox is %s then the project deletion settings visibility is set to %s',
+ (visible) => {
+ createComponent(shallowMountExtended, { deleteInactiveProjects: visible });
+
+ expect(findProjectDeletionSettings().exists()).toBe(visible);
+ },
+ );
+ });
+
+ describe('Minimum size for deletion', () => {
+ beforeEach(() => {
+ createComponent(mountExtended);
+ });
+
+ it('has the minimum size input', () => {
+ expect(findMinSizeInput().exists()).toBe(true);
+ });
+
+ it('has the field description', () => {
+ expect(findMinSizeGroup().text()).toContain('Delete inactive projects that exceed');
+ });
+
+ it('has the appended text on the field', () => {
+ expect(findMinSizeInputGroup().text()).toContain('MB');
+ });
+
+ it.each`
+ value | valid
+ ${'0'} | ${true}
+ ${'250'} | ${true}
+ ${'-1'} | ${false}
+ `(
+ 'when the minimum size input has a value of $value, then its validity should be $valid',
+ async ({ value, valid }) => {
+ await findMinSizeInput().find('input').setValue(value);
+
+ expect(findMinSizeGroup().classes('is-valid')).toBe(valid);
+ expect(findMinSizeInput().classes('is-valid')).toBe(valid);
+ },
+ );
+ });
+
+ describe('Delete project after', () => {
+ beforeEach(() => {
+ createComponent(mountExtended);
+ });
+
+ it('has the delete after months input', () => {
+ expect(findDeleteAfterMonthsInput().exists()).toBe(true);
+ });
+
+ it('has the appended text on the field', () => {
+ expect(findDeleteAfterMonthsInputGroup().text()).toContain('months');
+ });
+
+ it.each`
+ value | valid
+ ${'0'} | ${false}
+ ${'1'} | ${false /* Less than the default send warning email months */}
+ ${'2'} | ${true}
+ `(
+ 'when the delete after months input has a value of $value, then its validity should be $valid',
+ async ({ value, valid }) => {
+ await findDeleteAfterMonthsInput().find('input').setValue(value);
+
+ expect(findDeleteAfterMonthsGroup().classes('is-valid')).toBe(valid);
+ expect(findDeleteAfterMonthsInput().classes('is-valid')).toBe(valid);
+ },
+ );
+ });
+
+ describe('Send warning email', () => {
+ beforeEach(() => {
+ createComponent(mountExtended);
+ });
+
+ it('has the send warning email after months input', () => {
+ expect(findSendWarningEmailAfterMonthsInput().exists()).toBe(true);
+ });
+
+ it('has the field description', () => {
+ expect(findSendWarningEmailAfterMonthsGroup().text()).toContain(
+ 'Send email to maintainers after project is inactive for',
+ );
+ });
+
+ it('has the appended text on the field', () => {
+ expect(findSendWarningEmailAfterMonthsInputGroup().text()).toContain('months');
+ });
+
+ it.each`
+ value | valid
+ ${'2'} | ${true}
+ ${'0'} | ${false}
+ `(
+ 'when the minimum size input has a value of $value, then its validity should be $valid',
+ async ({ value, valid }) => {
+ await findSendWarningEmailAfterMonthsInput().find('input').setValue(value);
+
+ expect(findSendWarningEmailAfterMonthsGroup().classes('is-valid')).toBe(valid);
+ expect(findSendWarningEmailAfterMonthsInput().classes('is-valid')).toBe(valid);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 06dbadd6d3d..961fa96acdd 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -12,8 +12,8 @@ describe('initAdminUsersApp', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-users', JSON.stringify(users));
- el.setAttribute('data-paths', JSON.stringify(paths));
+ el.dataset.users = JSON.stringify(users);
+ el.dataset.paths = JSON.stringify(paths);
wrapper = createWrapper(initAdminUsersApp(el));
});
@@ -40,8 +40,8 @@ describe('initAdminUserActions', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-user', JSON.stringify(user));
- el.setAttribute('data-paths', JSON.stringify(paths));
+ el.dataset.user = JSON.stringify(user);
+ el.dataset.paths = JSON.stringify(paths);
wrapper = createWrapper(initAdminUserActions(el));
});
diff --git a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
index 703767dab47..f4cbc56be5c 100644
--- a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import UsageCounts from '~/analytics/usage_trends/components/usage_counts.vue';
@@ -30,7 +30,7 @@ describe('UsageCounts', () => {
wrapper.destroy();
});
- const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoading);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat);
describe('while loading', () => {
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 5f162f498c4..1f92010b771 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import createFlash from '~/flash';
jest.mock('~/flash');
@@ -608,30 +607,10 @@ describe('Api', () => {
},
]);
- return new Promise((resolve) => {
- Api.groupProjects(groupId, query, {}, (response) => {
- expect(response.length).toBe(1);
- expect(response[0].name).toBe('test');
- resolve();
- });
- });
- });
-
- it('uses flesh on error by default', async () => {
- const groupId = '123456';
- const query = 'dummy query';
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- const flashCallback = (callCount) => {
- expect(createFlash).toHaveBeenCalledTimes(callCount);
- createFlash.mockClear();
- };
-
- mock.onGet(expectedUrl).reply(500, null);
-
- const response = await Api.groupProjects(groupId, query, {}, () => {}).then(() => {
- flashCallback(1);
+ return Api.groupProjects(groupId, query, {}).then((response) => {
+ expect(response.data.length).toBe(1);
+ expect(response.data[0].name).toBe('test');
});
- expect(response).toBeUndefined();
});
it('NOT uses flesh on error with param useCustomErrorHandler', async () => {
@@ -640,7 +619,7 @@ describe('Api', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
mock.onGet(expectedUrl).reply(500, null);
- const apiCall = Api.groupProjects(groupId, query, {}, () => {}, true);
+ const apiCall = Api.groupProjects(groupId, query, {});
await expect(apiCall).rejects.toThrow();
});
});
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
index 0ff9d60f409..f9a6b2df662 100644
--- a/spec/frontend/authentication/two_factor_auth/index_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -15,8 +15,8 @@ describe('initRecoveryCodes', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('class', 'js-2fa-recovery-codes');
- el.setAttribute('data-codes', codesJsonString);
- el.setAttribute('data-profile-account-path', profileAccountPath);
+ el.dataset.codes = codesJsonString;
+ el.dataset.profileAccountPath = profileAccountPath;
document.body.appendChild(el);
wrapper = createWrapper(initRecoveryCodes());
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index 5d657745615..b14bc5122b9 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -57,6 +57,18 @@ describe('AwardsHandler', () => {
d: 'white question mark ornament',
u: '6.0',
},
+ thumbsup: {
+ c: 'people',
+ e: '👍',
+ d: 'thumbs up sign',
+ u: '6.0',
+ },
+ thumbsdown: {
+ c: 'people',
+ e: '👎',
+ d: 'thumbs down sign',
+ u: '6.0',
+ },
};
const openAndWaitForEmojiMenu = (sel = '.js-add-award') => {
@@ -296,6 +308,23 @@ describe('AwardsHandler', () => {
awardsHandler.searchEmojis('👼');
expect($('[data-name=angel]').is(':visible')).toBe(true);
});
+
+ it('should show positive intent emoji first', async () => {
+ await openAndWaitForEmojiMenu();
+
+ awardsHandler.searchEmojis('thumb');
+
+ const $menu = $('.emoji-menu');
+ const $thumbsUpItem = $menu.find('[data-name=thumbsup]');
+ const $thumbsDownItem = $menu.find('[data-name=thumbsdown]');
+
+ expect($thumbsUpItem.is(':visible')).toBe(true);
+ expect($thumbsDownItem.is(':visible')).toBe(true);
+
+ expect($thumbsUpItem.parents('.emoji-menu-list-item').index()).toBeLessThan(
+ $thumbsDownItem.parents('.emoji-menu-list-item').index(),
+ );
+ });
});
describe('emoji menu', () => {
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
new file mode 100644
index 00000000000..4f5ff797230
--- /dev/null
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
+
+Vue.use(Vuex);
+
+let wrapper;
+let publishReview;
+
+function factory() {
+ publishReview = jest.fn();
+
+ const store = new Vuex.Store({
+ getters: {
+ getNotesData: () => ({
+ markdownDocsPath: '/markdown/docs',
+ quickActionsDocsPath: '/quickactions/docs',
+ }),
+ getNoteableData: () => ({ id: 1, preview_note_path: '/preview' }),
+ noteableType: () => 'merge_request',
+ },
+ modules: {
+ batchComments: {
+ namespaced: true,
+ actions: {
+ publishReview,
+ },
+ },
+ },
+ });
+ wrapper = mountExtended(SubmitDropdown, {
+ store,
+ });
+}
+
+const findCommentTextarea = () => wrapper.findByTestId('comment-textarea');
+const findSubmitButton = () => wrapper.findByTestId('submit-review-button');
+const findForm = () => wrapper.findByTestId('submit-gl-form');
+
+describe('Batch comments submit dropdown', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('calls publishReview with note data', async () => {
+ factory();
+
+ findCommentTextarea().setValue('Hello world');
+
+ await findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+
+ expect(publishReview).toHaveBeenCalledWith(expect.anything(), {
+ noteable_type: 'merge_request',
+ noteable_id: 1,
+ note: 'Hello world',
+ });
+ });
+
+ it('sets submit dropdown to loading', async () => {
+ factory();
+
+ findCommentTextarea().setValue('Hello world');
+
+ await findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+
+ expect(findSubmitButton().props('loading')).toBe(true);
+ });
+});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index e9535d8cc12..172b510645d 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -179,6 +179,16 @@ describe('Batch comments store actions', () => {
});
});
+ it('calls service with notes data', () => {
+ jest.spyOn(axios, 'post');
+
+ return actions
+ .publishReview({ dispatch, commit, getters, rootGetters }, { note: 'test' })
+ .then(() => {
+ expect(axios.post.mock.calls[0]).toEqual(['http://test.host', { note: 'test' }]);
+ });
+ });
+
it('dispatches error commits', () => {
mock.onAny().reply(500);
diff --git a/spec/frontend/behaviors/markdown/render_mermaid_spec.js b/spec/frontend/behaviors/markdown/render_mermaid_spec.js
deleted file mode 100644
index 51a345cab0e..00000000000
--- a/spec/frontend/behaviors/markdown/render_mermaid_spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { initMermaid } from '~/behaviors/markdown/render_mermaid';
-import * as ColorUtils from '~/lib/utils/color_utils';
-
-describe('Render mermaid diagrams for Gitlab Flavoured Markdown', () => {
- it.each`
- darkMode | expectedTheme
- ${false} | ${'neutral'}
- ${true} | ${'dark'}
- `('is $darkMode $expectedTheme', async ({ darkMode, expectedTheme }) => {
- jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => darkMode);
-
- const mermaid = {
- initialize: jest.fn(),
- };
-
- await initMermaid(mermaid);
-
- expect(mermaid.initialize).toHaveBeenCalledTimes(1);
- expect(mermaid.initialize).toHaveBeenCalledWith(
- expect.objectContaining({
- theme: expectedTheme,
- }),
- );
- });
-});
diff --git a/spec/frontend/blob/blob_file_dropzone_spec.js b/spec/frontend/blob/blob_file_dropzone_spec.js
deleted file mode 100644
index d6fc824258b..00000000000
--- a/spec/frontend/blob/blob_file_dropzone_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import BlobFileDropzone from '~/blob/blob_file_dropzone';
-
-describe('BlobFileDropzone', () => {
- let dropzone;
- let replaceFileButton;
-
- beforeEach(() => {
- loadHTMLFixture('blob/show.html');
- const form = $('.js-upload-blob-form');
- // eslint-disable-next-line no-new
- new BlobFileDropzone(form, 'POST');
- dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone;
- dropzone.processQueue = jest.fn();
- replaceFileButton = $('#submit-all');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('submit button', () => {
- it('requires file', () => {
- jest.spyOn(window, 'alert').mockImplementation(() => {});
-
- replaceFileButton.click();
-
- expect(window.alert).toHaveBeenCalled();
- });
-
- it('is disabled while uploading', () => {
- jest.spyOn(window, 'alert').mockImplementation(() => {});
-
- const file = new File([], 'some-file.jpg');
- const fakeEvent = $.Event('drop', {
- dataTransfer: { files: [file] },
- });
-
- dropzone.listeners[0].events.drop(fakeEvent);
-
- replaceFileButton.click();
-
- expect(window.alert).not.toHaveBeenCalled();
- expect(replaceFileButton.is(':disabled')).toEqual(true);
- expect(dropzone.processQueue).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
index d698ee72ea4..fdbb9bdd0d0 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
@@ -7,7 +7,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
<file-icon-stub
aria-hidden="true"
- cssclasses="mr-2"
+ cssclasses="gl-mr-3"
filemode=""
filename="foo/bar/dummy.md"
size="16"
@@ -32,7 +32,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
/>
<small
- class="mr-2"
+ class="gl-mr-3"
>
a lot
</small>
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index af605b257de..aa538facae2 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -88,6 +88,14 @@ describe('Blob Header Default Actions', () => {
expect(findCopyButton().exists()).toBe(false);
expect(findViewRawButton().exists()).toBe(false);
});
+
+ it('emits a copy event if overrideCopy is set to true', () => {
+ createComponent({ overrideCopy: true });
+ jest.spyOn(wrapper.vm, '$emit');
+ findCopyButton().vm.$emit('click');
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('copy');
+ });
});
describe('view on environment button', () => {
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 358ac31819c..2cbac809a0d 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -11,7 +11,7 @@ function createComponent() {
}
async function setLoaded(loaded) {
- document.querySelector('.blob-viewer').setAttribute('data-loaded', loaded);
+ document.querySelector('.blob-viewer').dataset.loaded = loaded;
await nextTick();
}
@@ -53,7 +53,7 @@ describe('Markdown table of contents component', () => {
it('does not show dropdown when viewing non-rich content', async () => {
createComponent();
- document.querySelector('.blob-viewer').setAttribute('data-type', 'simple');
+ document.querySelector('.blob-viewer').dataset.type = 'simple';
await setLoaded(true);
diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js
index ff96193a20c..9364f76da5e 100644
--- a/spec/frontend/blob/csv/csv_viewer_spec.js
+++ b/spec/frontend/blob/csv/csv_viewer_spec.js
@@ -44,7 +44,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
describe('when the CSV contains errors', () => {
it('should render alert with correct props', async () => {
createComponent({ csv: brokenCsv });
- await nextTick;
+ await nextTick();
expect(findAlert().props()).toMatchObject({
papaParseErrors: [{ code: 'UndetectableDelimiter' }],
@@ -55,14 +55,14 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
describe('when the CSV contains no errors', () => {
it('should not render alert', async () => {
createComponent();
- await nextTick;
+ await nextTick();
expect(findAlert().exists()).toBe(false);
});
it('renders the CSV table with the correct attributes', async () => {
createComponent();
- await nextTick;
+ await nextTick();
expect(findCsvTable().attributes()).toMatchObject({
'empty-text': 'No CSV data to display.',
@@ -72,7 +72,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
it('renders the CSV table with the correct content', async () => {
createComponent({ mountFunction: mount });
- await nextTick;
+ await nextTick();
expect(getAllByRole(wrapper.element, 'row', { name: /One/i })).toHaveLength(1);
expect(getAllByRole(wrapper.element, 'row', { name: /Two/i })).toHaveLength(1);
@@ -93,7 +93,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
skipEmptyLines: true,
complete: expect.any(Function),
});
- await nextTick;
+ await nextTick();
expect(wrapper.vm.items).toEqual(validCsv.split(','));
});
});
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 5f6baf3f63d..b2559af182b 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -80,9 +80,9 @@ describe('Blob viewer', () => {
return asyncClick()
.then(() => asyncClick())
.then(() => {
- expect(
- document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
- ).toBe('true');
+ expect(document.querySelector('.blob-viewer[data-type="simple"]').dataset.loaded).toBe(
+ 'true',
+ );
});
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index f1964daa8b2..c13f7caba76 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -20,8 +20,6 @@ describe('Board Column Component', () => {
};
const createComponent = ({ listType = ListType.backlog, collapsed = false } = {}) => {
- const boardId = '1';
-
const listMock = {
...listObj,
listType,
@@ -39,9 +37,6 @@ describe('Board Column Component', () => {
disabled: false,
list: listMock,
},
- provide: {
- boardId,
- },
});
};
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 6a659623b53..fdc16b46167 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,4 +1,6 @@
import { GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -8,7 +10,6 @@ import { formType } from '~/boards/constants';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
-import { createStore } from '~/boards/stores';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -16,6 +17,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
}));
+Vue.use(Vuex);
+
const currentBoard = {
id: 'gid://gitlab/Board/1',
name: 'test',
@@ -46,11 +49,18 @@ describe('BoardForm', () => {
const findDeleteConfirmation = () => wrapper.findByTestId('delete-confirmation-message');
const findInput = () => wrapper.find('#board-new-name');
- const store = createStore({
+ const setBoardMock = jest.fn();
+ const setErrorMock = jest.fn();
+
+ const store = new Vuex.Store({
getters: {
isGroupBoard: () => true,
isProjectBoard: () => false,
},
+ actions: {
+ setBoard: setBoardMock,
+ setError: setErrorMock,
+ },
});
const createComponent = (props, data) => {
@@ -168,7 +178,7 @@ describe('BoardForm', () => {
expect(mutate).not.toHaveBeenCalled();
});
- it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
+ it('calls a correct GraphQL mutation and sets board in state', async () => {
createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
@@ -184,13 +194,12 @@ describe('BoardForm', () => {
});
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('test-path');
+ expect(setBoardMock).toHaveBeenCalledTimes(1);
});
- it('shows a GlAlert if GraphQL mutation fails', async () => {
+ it('sets error in state if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.new });
- jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
fillForm();
@@ -199,8 +208,8 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalled();
await waitForPromises();
- expect(visitUrl).not.toHaveBeenCalled();
- expect(wrapper.vm.setError).toHaveBeenCalled();
+ expect(setBoardMock).not.toHaveBeenCalled();
+ expect(setErrorMock).toHaveBeenCalled();
});
});
});
@@ -256,7 +265,8 @@ describe('BoardForm', () => {
});
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('test-path');
+ expect(setBoardMock).toHaveBeenCalledTimes(1);
+ expect(global.window.location.href).not.toContain('?group_by=epic');
});
it('calls GraphQL mutation with correct parameters when issues are grouped by epic', async () => {
@@ -282,13 +292,13 @@ describe('BoardForm', () => {
});
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('test-path?group_by=epic');
+ expect(setBoardMock).toHaveBeenCalledTimes(1);
+ expect(global.window.location.href).toContain('?group_by=epic');
});
- it('shows a GlAlert if GraphQL mutation fails', async () => {
+ it('sets error in state if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.edit });
- jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findInput().trigger('keyup.enter', { metaKey: true });
@@ -297,8 +307,8 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalled();
await waitForPromises();
- expect(visitUrl).not.toHaveBeenCalled();
- expect(wrapper.vm.setError).toHaveBeenCalled();
+ expect(setBoardMock).not.toHaveBeenCalled();
+ expect(setErrorMock).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index f60d04af4fc..d91e81fe4d0 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -2,11 +2,10 @@ import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import { BoardType } from '~/boards/constants';
-import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
-import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
@@ -15,8 +14,7 @@ import defaultStore from '~/boards/stores';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
- mockGroupBoardResponse,
- mockProjectBoardResponse,
+ mockBoard,
mockGroupAllBoardsResponse,
mockProjectAllBoardsResponse,
mockGroupRecentBoardsResponse,
@@ -49,6 +47,7 @@ describe('BoardsSelector', () => {
},
state: {
boardType: isGroupBoard ? 'group' : 'project',
+ board: mockBoard,
},
});
};
@@ -65,9 +64,6 @@ describe('BoardsSelector', () => {
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
- const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
-
const projectBoardsQueryHandlerSuccess = jest
.fn()
.mockResolvedValue(mockProjectAllBoardsResponse);
@@ -92,8 +88,6 @@ describe('BoardsSelector', () => {
projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
} = {}) => {
fakeApollo = createMockApollo([
- [projectBoardQuery, projectBoardQueryHandlerSuccess],
- [groupBoardQuery, groupBoardQueryHandlerSuccess],
[projectBoardsQuery, projectBoardsQueryHandler],
[groupBoardsQuery, groupBoardsQueryHandlerSuccess],
[projectRecentBoardsQuery, projectRecentBoardsQueryHandler],
@@ -133,12 +127,13 @@ describe('BoardsSelector', () => {
describe('loading', () => {
// we are testing loading state, so don't resolve responses until after the tests
afterEach(async () => {
- await nextTick();
+ await waitForPromises();
});
- it('shows loading spinner', () => {
+ it('shows loading spinner', async () => {
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
+ await nextTick();
expect(getLoadingIcon().exists()).toBe(true);
expect(getDropdownHeaders()).toHaveLength(0);
@@ -251,23 +246,4 @@ describe('BoardsSelector', () => {
expect(notCalledHandler).not.toHaveBeenCalled();
});
});
-
- describe('fetching current board', () => {
- it.each`
- boardType | queryHandler | notCalledHandler
- ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
- ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
- `('fetches $boardType board', async ({ boardType, queryHandler, notCalledHandler }) => {
- createStore({
- isProjectBoard: boardType === BoardType.project,
- isGroupBoard: boardType === BoardType.group,
- });
- createComponent();
-
- await nextTick();
-
- expect(queryHandler).toHaveBeenCalled();
- expect(notCalledHandler).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 26ad9790840..6ec39be5d29 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -144,30 +144,6 @@ export const mockProjectRecentBoardsResponse = {
},
};
-export const mockGroupBoardResponse = {
- data: {
- workspace: {
- board: {
- id: 'gid://gitlab/Board/1',
- name: 'Development',
- },
- __typename: 'Group',
- },
- },
-};
-
-export const mockProjectBoardResponse = {
- data: {
- workspace: {
- board: {
- id: 'gid://gitlab/Board/2',
- name: 'Development',
- },
- __typename: 'Project',
- },
- },
-};
-
export const mockAssigneesList = [
{
id: 2,
@@ -802,3 +778,15 @@ export const boardListQueryResponse = (issuesCount = 20) => ({
},
},
});
+
+export const epicBoardListQueryResponse = (totalWeight = 5) => ({
+ data: {
+ epicBoardList: {
+ __typename: 'EpicList',
+ id: 'gid://gitlab/Boards::EpicList/3',
+ metadata: {
+ totalWeight,
+ },
+ },
+ },
+});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index eacf9db191e..e48b946ff1b 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -77,7 +77,7 @@ describe('fetchBoard', () => {
},
};
- it('should commit mutation RECEIVE_BOARD_SUCCESS and dispatch setBoardConfig on success', async () => {
+ it('should commit mutation REQUEST_CURRENT_BOARD and dispatch setBoard on success', async () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
await testAction({
@@ -85,11 +85,10 @@ describe('fetchBoard', () => {
payload,
expectedMutations: [
{
- type: types.RECEIVE_BOARD_SUCCESS,
- payload: mockBoard,
+ type: types.REQUEST_CURRENT_BOARD,
},
],
- expectedActions: [{ type: 'setBoardConfig', payload: mockBoard }],
+ expectedActions: [{ type: 'setBoard', payload: mockBoard }],
});
});
@@ -101,6 +100,9 @@ describe('fetchBoard', () => {
payload,
expectedMutations: [
{
+ type: types.REQUEST_CURRENT_BOARD,
+ },
+ {
type: types.RECEIVE_BOARD_FAILURE,
},
],
@@ -133,6 +135,20 @@ describe('setBoardConfig', () => {
});
});
+describe('setBoard', () => {
+ it('dispatches setBoardConfig', () => {
+ return testAction({
+ action: actions.setBoard,
+ payload: mockBoard,
+ expectedMutations: [{ type: types.RECEIVE_BOARD_SUCCESS, payload: mockBoard }],
+ expectedActions: [
+ { type: 'setBoardConfig', payload: mockBoard },
+ { type: 'performSearch', payload: { resetLists: true } },
+ ],
+ });
+ });
+});
+
describe('setFilters', () => {
it.each([
[
@@ -172,7 +188,11 @@ describe('performSearch', () => {
{},
{},
[],
- [{ type: 'setFilters', payload: {} }, { type: 'fetchLists' }, { type: 'resetIssues' }],
+ [
+ { type: 'setFilters', payload: {} },
+ { type: 'fetchLists', payload: { resetLists: false } },
+ { type: 'resetIssues' },
+ ],
);
});
});
@@ -955,10 +975,6 @@ describe('fetchItemsForList', () => {
state,
[
{
- type: types.RESET_ITEMS_FOR_LIST,
- payload: listId,
- },
- {
type: types.REQUEST_ITEMS_FOR_LIST,
payload: { listId, fetchNext: false },
},
@@ -980,10 +996,6 @@ describe('fetchItemsForList', () => {
state,
[
{
- type: types.RESET_ITEMS_FOR_LIST,
- payload: listId,
- },
- {
type: types.REQUEST_ITEMS_FOR_LIST,
payload: { listId, fetchNext: false },
},
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 738737bf4b6..7d79993a0ee 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -34,6 +34,14 @@ describe('Board Store Mutations', () => {
state = defaultState();
});
+ describe('REQUEST_CURRENT_BOARD', () => {
+ it('Should set isBoardLoading state to true', () => {
+ mutations[types.REQUEST_CURRENT_BOARD](state);
+
+ expect(state.isBoardLoading).toBe(true);
+ });
+ });
+
describe('RECEIVE_BOARD_SUCCESS', () => {
it('Should set board to state', () => {
mutations[types.RECEIVE_BOARD_SUCCESS](state, mockBoard);
@@ -292,24 +300,6 @@ describe('Board Store Mutations', () => {
});
});
- describe('RESET_ITEMS_FOR_LIST', () => {
- it('should remove issues from boardItemsByListId state', () => {
- const listId = 'gid://gitlab/List/1';
- const boardItemsByListId = {
- [listId]: [mockIssue.id],
- };
-
- state = {
- ...state,
- boardItemsByListId,
- };
-
- mutations[types.RESET_ITEMS_FOR_LIST](state, listId);
-
- expect(state.boardItemsByListId[listId]).toEqual([]);
- });
- });
-
describe('REQUEST_ITEMS_FOR_LIST', () => {
const listId = 'gid://gitlab/List/1';
const boardItemsByListId = {
diff --git a/spec/frontend/cascading_settings/components/lock_popovers_spec.js b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
index 585e6ac505b..182e3c1c8ff 100644
--- a/spec/frontend/cascading_settings/components/lock_popovers_spec.js
+++ b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
@@ -21,12 +21,12 @@ describe('LockPopovers', () => {
};
if (lockedByApplicationSetting) {
- popoverMountEl.setAttribute('data-popover-data', JSON.stringify(popoverData));
+ popoverMountEl.dataset.popoverData = JSON.stringify(popoverData);
} else if (lockedByAncestor) {
- popoverMountEl.setAttribute(
- 'data-popover-data',
- JSON.stringify({ ...popoverData, ancestor_namespace: mockNamespace }),
- );
+ popoverMountEl.dataset.popoverData = JSON.stringify({
+ ...popoverData,
+ ancestor_namespace: mockNamespace,
+ });
}
document.body.appendChild(popoverMountEl);
diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
index e7e4897abfa..b3e23ba4201 100644
--- a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
+import LegacyCiEnvironmentsDropdown from '~/ci_variable_list/components/legacy_ci_environments_dropdown.vue';
Vue.use(Vuex);
@@ -20,7 +20,7 @@ describe('Ci environments dropdown', () => {
},
});
- wrapper = mount(CiEnvironmentsDropdown, {
+ wrapper = mount(LegacyCiEnvironmentsDropdown, {
store,
propsData: {
value: term,
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
index d26378d9382..42c6501dcce 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
@@ -4,7 +4,7 @@ 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 CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
+import LegacyCiVariableModal from '~/ci_variable_list/components/legacy_ci_variable_modal.vue';
import {
AWS_ACCESS_KEY_ID,
EVENT_LABEL,
@@ -30,7 +30,7 @@ describe('Ci variable modal', () => {
isGroup: options.isGroup,
environmentScopeLink: '/help/environments',
});
- wrapper = method(CiVariableModal, {
+ wrapper = method(LegacyCiVariableModal, {
attachTo: document.body,
stubs: {
GlModal: ModalStub,
@@ -42,10 +42,7 @@ describe('Ci variable modal', () => {
const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown);
const findModal = () => wrapper.find(ModalStub);
- const findAddorUpdateButton = () =>
- findModal()
- .findAll(GlButton)
- .wrappers.find((button) => button.props('variant') === 'confirm');
+ const findAddorUpdateButton = () => findModal().find('[data-testid="ciUpdateOrAddVariableBtn"]');
const deleteVariableButton = () =>
findModal()
.findAll(GlButton)
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
index 13e417940a8..9c941f99982 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import LegacyCiVariableSettings from '~/ci_variable_list/components/legacy_ci_variable_settings.vue';
import createStore from '~/ci_variable_list/store';
Vue.use(Vuex);
@@ -15,7 +15,7 @@ describe('Ci variable table', () => {
store = createStore();
store.state.isGroup = groupState;
jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = shallowMount(CiVariableSettings, {
+ wrapper = shallowMount(LegacyCiVariableSettings, {
store,
});
};
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
index 62f9ae4eb4e..310afc8003a 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+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';
@@ -14,7 +14,7 @@ describe('Ci variable table', () => {
const createComponent = () => {
store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = mountExtended(CiVariableTable, {
+ wrapper = mountExtended(LegacyCiVariableTable, {
attachTo: document.body,
store,
});
diff --git a/spec/frontend/clusters/agents/components/create_token_button_spec.js b/spec/frontend/clusters/agents/components/create_token_button_spec.js
index b9a3a851e57..fb1a3aa2963 100644
--- a/spec/frontend/clusters/agents/components/create_token_button_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_button_spec.js
@@ -11,6 +11,7 @@ import {
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
MAX_LIST_COUNT,
+ CREATE_TOKEN_MODAL,
} from '~/clusters/agents/constants';
import createNewAgentToken from '~/clusters/agents/graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
@@ -231,7 +232,11 @@ describe('CreateTokenButton', () => {
});
it('shows agent instructions', () => {
- expect(findAgentInstructions().exists()).toBe(true);
+ expect(findAgentInstructions().props()).toMatchObject({
+ agentName,
+ agentToken: 'token-secret',
+ modalId: CREATE_TOKEN_MODAL,
+ });
});
it('renders a close button', () => {
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index 42d81900911..46ee123a12d 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -1,167 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Remove cluster confirmation modal renders splitbutton with modal included 1`] = `
+exports[`Remove cluster confirmation modal renders buttons with modal included 1`] = `
<div
- class="gl-display-flex gl-justify-content-end"
+ class="gl-display-flex"
>
- <div
- class="dropdown b-dropdown gl-new-dropdown btn-group"
- menu-class="dropdown-menu-large"
+ <button
+ class="btn gl-mr-3 btn-danger btn-md gl-button"
+ data-testid="remove-integration-and-resources-button"
+ type="button"
>
- <button
- class="btn btn-danger btn-md gl-button split-content-button"
- type="button"
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
>
- <!---->
-
- <!---->
-
- <span
- class="gl-new-dropdown-button-text"
- >
- Remove integration and resources
- </span>
-
- <!---->
- </button>
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-danger btn-md gl-button gl-dropdown-toggle dropdown-toggle-split"
- type="button"
- >
- <span
- class="sr-only"
- >
- Toggle dropdown
- </span>
- </button>
- <ul
- class="dropdown-menu dropdown-menu-large"
- role="menu"
- tabindex="-1"
+
+ Remove integration and resources
+
+ </span>
+ </button>
+
+ <button
+ class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ data-testid="remove-integration-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
>
- <div
- class="gl-new-dropdown-inner"
- >
- <!---->
-
- <!---->
-
- <div
- class="gl-new-dropdown-contents"
- >
- <!---->
-
- <li
- class="gl-new-dropdown-item"
- role="presentation"
- >
- <button
- class="dropdown-item"
- role="menuitem"
- type="button"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon gl-mt-3 gl-align-self-start"
- data-testid="dropdown-item-checkbox"
- role="img"
- >
- <use
- href="#mobile-issue-close"
- />
- </svg>
-
- <!---->
-
- <!---->
-
- <div
- class="gl-new-dropdown-item-text-wrapper"
- >
- <p
- class="gl-new-dropdown-item-text-primary"
- >
- <strong>
- Remove integration and resources
- </strong>
-
- <div>
- Deletes all GitLab resources attached to this cluster during removal
- </div>
- </p>
-
- <!---->
- </div>
-
- <!---->
- </button>
- </li>
-
- <li
- class="gl-new-dropdown-divider"
- role="presentation"
- >
- <hr
- aria-orientation="horizontal"
- class="dropdown-divider"
- role="separator"
- />
- </li>
- <li
- class="gl-new-dropdown-item"
- role="presentation"
- >
- <button
- class="dropdown-item"
- role="menuitem"
- type="button"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden gl-mt-3 gl-align-self-start"
- data-testid="dropdown-item-checkbox"
- role="img"
- >
- <use
- href="#mobile-issue-close"
- />
- </svg>
-
- <!---->
-
- <!---->
-
- <div
- class="gl-new-dropdown-item-text-wrapper"
- >
- <p
- class="gl-new-dropdown-item-text-primary"
- >
- <strong>
- Remove integration
- </strong>
-
- <div>
- Removes cluster from project but keeps associated resources
- </div>
- </p>
-
- <!---->
- </div>
-
- <!---->
- </button>
- </li>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </ul>
- </div>
+
+ Remove integration
+
+ </span>
+ </button>
<!---->
</div>
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
index 173fefe6167..53683af893a 100644
--- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -3,7 +3,6 @@ import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue';
-import SplitButton from '~/vue_shared/components/split_button.vue';
describe('Remove cluster confirmation modal', () => {
let wrapper;
@@ -24,14 +23,17 @@ describe('Remove cluster confirmation modal', () => {
wrapper = null;
});
- it('renders splitbutton with modal included', () => {
+ it('renders buttons with modal included', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
- describe('split button dropdown', () => {
+ describe('two buttons', () => {
const findModal = () => wrapper.findComponent(GlModal);
- const findSplitButton = () => wrapper.findComponent(SplitButton);
+ const findRemoveIntegrationButton = () =>
+ wrapper.find('[data-testid="remove-integration-button"]');
+ const findRemoveIntegrationAndResourcesButton = () =>
+ wrapper.find('[data-testid="remove-integration-and-resources-button"]');
beforeEach(() => {
createComponent({
@@ -41,8 +43,8 @@ describe('Remove cluster confirmation modal', () => {
jest.spyOn(findModal().vm, 'show').mockReturnValue();
});
- it('opens modal with "cleanup" option', async () => {
- findSplitButton().vm.$emit('remove-cluster-and-cleanup');
+ it('open modal with "cleanup" option', async () => {
+ findRemoveIntegrationAndResourcesButton().trigger('click');
await nextTick();
@@ -53,8 +55,8 @@ describe('Remove cluster confirmation modal', () => {
);
});
- it('opens modal without "cleanup" option', async () => {
- findSplitButton().vm.$emit('remove-cluster');
+ it('open modal without "cleanup" option', async () => {
+ findRemoveIntegrationButton().trigger('click');
await nextTick();
@@ -71,8 +73,8 @@ describe('Remove cluster confirmation modal', () => {
});
it('renders regular button instead', () => {
- expect(findSplitButton().exists()).toBe(false);
- expect(wrapper.find('[data-testid="btnRemove"]').exists()).toBe(true);
+ expect(findRemoveIntegrationAndResourcesButton().exists()).toBe(false);
+ expect(findRemoveIntegrationButton().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index cdd94d33545..8d3130b45a6 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -7,6 +7,7 @@ import CodeBlock from '~/vue_shared/components/code_block.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
const kasAddress = 'kas.example.com';
+const agentName = 'my-agent';
const agentToken = 'agent-token';
const kasVersion = '15.0.0';
const modalId = INSTALL_AGENT_MODAL_ID;
@@ -26,6 +27,7 @@ describe('InstallAgentModal', () => {
};
const propsData = {
+ agentName,
agentToken,
modalId,
};
@@ -61,7 +63,12 @@ describe('InstallAgentModal', () => {
it('renders a copy button', () => {
expect(findCopyButton().props()).toMatchObject({
title: 'Copy command',
- text: generateAgentRegistrationCommand(agentToken, kasAddress, kasVersion),
+ text: generateAgentRegistrationCommand({
+ name: agentName,
+ token: agentToken,
+ version: kasVersion,
+ address: kasAddress,
+ }),
modalId,
});
});
@@ -71,6 +78,7 @@ describe('InstallAgentModal', () => {
});
it('shows code block with agent installation command', () => {
+ expect(findCodeBlock().props('code')).toContain(`helm upgrade --install ${agentName}`);
expect(findCodeBlock().props('code')).toContain(`--set config.token=${agentToken}`);
expect(findCodeBlock().props('code')).toContain(`--set config.kasAddress=${kasAddress}`);
expect(findCodeBlock().props('code')).toContain(`--set image.tag=v${kasVersion}`);
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 3f3f5e0daf6..c150a7f05d0 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -1,9 +1,4 @@
-import {
- GlLoadingIcon,
- GlPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlTableLite,
-} from '@gitlab/ui';
+import { GlLoadingIcon, GlPagination, GlSkeletonLoader, GlTableLite } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -171,7 +166,7 @@ describe('Clusters', () => {
if (nodeSize) {
expect(size.text()).toBe(nodeSize);
} else {
- expect(size.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(size.findComponent(GlSkeletonLoader).exists()).toBe(true);
}
});
});
@@ -195,7 +190,7 @@ describe('Clusters', () => {
const size = sizes.at(lineNumber);
expect(size.text()).toContain(nodeText);
- expect(size.find(GlSkeletonLoading).exists()).toBe(false);
+ expect(size.findComponent(GlSkeletonLoader).exists()).toBe(false);
});
});
@@ -221,12 +216,12 @@ describe('Clusters', () => {
describe('cluster CPU', () => {
it.each`
clusterCpu | lineNumber
- ${''} | ${0}
+ ${'Loading'} | ${0}
${'1.93 (87% free)'} | ${1}
${'3.87 (86% free)'} | ${2}
${'(% free)'} | ${3}
${'(% free)'} | ${4}
- ${''} | ${5}
+ ${'Loading'} | ${5}
`('renders total cpu for each cluster', ({ clusterCpu, lineNumber }) => {
const clusterCpus = findTable().findAll('td:nth-child(4)');
const cpuData = clusterCpus.at(lineNumber);
@@ -238,12 +233,12 @@ describe('Clusters', () => {
describe('cluster Memory', () => {
it.each`
clusterMemory | lineNumber
- ${''} | ${0}
+ ${'Loading'} | ${0}
${'5.92 (78% free)'} | ${1}
${'12.86 (79% free)'} | ${2}
${'(% free)'} | ${3}
${'(% free)'} | ${4}
- ${''} | ${5}
+ ${'Loading'} | ${5}
`('renders total memory for each cluster', ({ clusterMemory, lineNumber }) => {
const clusterMemories = findTable().findAll('td:nth-child(5)');
const memoryData = clusterMemories.at(lineNumber);
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 38f653509a8..29884675b24 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -15,6 +15,7 @@ import {
EVENT_ACTIONS_SELECT,
MODAL_TYPE_EMPTY,
MODAL_TYPE_REGISTER,
+ INSTALL_AGENT_MODAL_ID,
} from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import getAgentConfigurations from '~/clusters_list/graphql/queries/agent_configurations.query.graphql';
@@ -222,7 +223,11 @@ describe('InstallAgentModal', () => {
});
it('shows agent instructions', () => {
- expect(findAgentInstructions().exists()).toBe(true);
+ expect(findAgentInstructions().props()).toMatchObject({
+ agentName: 'agent-name',
+ agentToken: 'mock-agent-token',
+ modalId: INSTALL_AGENT_MODAL_ID,
+ });
});
describe('error creating agent', () => {
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index c47a9e697b6..8eee61d1342 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -195,8 +195,8 @@ describe('Code navigation actions', () => {
it('commits SET_CURRENT_DEFINITION with LSIF data', () => {
target.classList.add('js-code-navigation');
- target.setAttribute('data-line-index', '0');
- target.setAttribute('data-char-index', '0');
+ target.dataset.lineIndex = '0';
+ target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
@@ -218,8 +218,8 @@ describe('Code navigation actions', () => {
it('adds hll class to target element', () => {
target.classList.add('js-code-navigation');
- target.setAttribute('data-line-index', '0');
- target.setAttribute('data-char-index', '0');
+ target.dataset.lineIndex = '0';
+ target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
@@ -243,8 +243,8 @@ describe('Code navigation actions', () => {
it('caches current target element', () => {
target.classList.add('js-code-navigation');
- target.setAttribute('data-line-index', '0');
- target.setAttribute('data-char-index', '0');
+ target.dataset.lineIndex = '0';
+ target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index 53991349ee5..4224fb6be2a 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -31,9 +31,9 @@ describe('ConfirmModal', () => {
buttons.forEach((x) => {
const button = document.createElement('button');
button.setAttribute('class', 'js-confirm-modal-button');
- button.setAttribute('data-path', x.path);
- button.setAttribute('data-method', x.method);
- button.setAttribute('data-modal-attributes', JSON.stringify(x.modalAttributes));
+ button.dataset.path = x.path;
+ button.dataset.method = x.method;
+ button.dataset.modalAttributes = JSON.stringify(x.modalAttributes);
button.innerHTML = 'Action';
buttonContainer.appendChild(button);
});
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
index 7abd6b422ad..b54f7cf17c8 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -16,15 +16,13 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
<!---->
<li role=\\"presentation\\" class=\\"gl-px-3!\\">
<form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
- <div placeholder=\\"Link URL\\">
- <div role=\\"group\\" class=\\"input-group\\">
- <!---->
- <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"form-control gl-form-input\\">
- <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
- <!---->
- </div>
+ <div role=\\"group\\" class=\\"input-group\\" placeholder=\\"Link URL\\">
+ <!---->
+ <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"form-control gl-form-input\\">
+ <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
+ <!---->
+ <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
+ <!---->
</div>
</form>
</li>
diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
index 3a15ea45f40..646d068e795 100644
--- a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js
@@ -1,21 +1,33 @@
import { BubbleMenu } from '@tiptap/vue-2';
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-import Vue from 'vue';
+import {
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlFormInput,
+} from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Diagram from '~/content_editor/extensions/diagram';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
import { createTestEditor, emitEditorEvent } from '../../test_utils';
+const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
+
describe('content_editor/components/bubble_menus/code_block', () => {
let wrapper;
let tiptapEditor;
+ let contentEditor;
let bubbleMenu;
let eventHub;
const buildEditor = () => {
- tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
+ tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] });
+ contentEditor = { renderDiagram: jest.fn() };
eventHub = eventHubFactory();
};
@@ -23,8 +35,12 @@ describe('content_editor/components/bubble_menus/code_block', () => {
wrapper = mountExtended(CodeBlockBubbleMenu, {
provide: {
tiptapEditor,
+ contentEditor,
eventHub,
},
+ stubs: {
+ GlDropdownItem: stubComponent(GlDropdownItem),
+ },
});
};
@@ -36,7 +52,7 @@ describe('content_editor/components/bubble_menus/code_block', () => {
checked: x.props('isChecked'),
}));
- beforeEach(() => {
+ beforeEach(async () => {
buildEditor();
buildWrapper();
});
@@ -73,6 +89,15 @@ describe('content_editor/components/bubble_menus/code_block', () => {
expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Javascript');
});
+ it('selects diagram sytnax for mermaid', async () => {
+ tiptapEditor.commands.insertContent('<pre lang="mermaid">test</pre>');
+ bubbleMenu = wrapper.findComponent(BubbleMenu);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Diagram (mermaid)');
+ });
+
it("selects Custom (syntax) if the language doesn't exist in the list", async () => {
tiptapEditor.commands.insertContent('<pre lang="nomnoml">test</pre>');
bubbleMenu = wrapper.findComponent(BubbleMenu);
@@ -104,22 +129,57 @@ describe('content_editor/components/bubble_menus/code_block', () => {
});
});
+ describe('preview button', () => {
+ it('does not appear for a regular code block', async () => {
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+
+ expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false);
+ });
+
+ it.each`
+ diagramType | diagramCode
+ ${'mermaid'} | ${'<pre lang="mermaid">graph TD;\n A-->B;</pre>'}
+ ${'nomnoml'} | ${'<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">'}
+ `('toggles preview for a $diagramType diagram', async ({ diagramType, diagramCode }) => {
+ tiptapEditor.commands.insertContent(diagramCode);
+
+ await nextTick();
+ await wrapper.findByTestId('preview-diagram').vm.$emit('click');
+
+ expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({
+ isDiagram: true,
+ language: diagramType,
+ showPreview: false,
+ });
+
+ await wrapper.findByTestId('preview-diagram').vm.$emit('click');
+
+ expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({
+ isDiagram: true,
+ language: diagramType,
+ showPreview: true,
+ });
+ });
+ });
+
describe('when opened and search is changed', () => {
beforeEach(async () => {
tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'js');
- await Vue.nextTick();
+ await nextTick();
});
it('shows dropdown items', () => {
- expect(findDropdownItemsData()).toEqual([
- { text: 'Javascript', visible: true, checked: true },
- { text: 'Java', visible: true, checked: false },
- { text: 'Javascript', visible: false, checked: false },
- { text: 'JSON', visible: true, checked: false },
- ]);
+ expect(findDropdownItemsData()).toEqual(
+ expect.arrayContaining([
+ { text: 'Javascript', visible: true, checked: true },
+ { text: 'Java', visible: true, checked: false },
+ { text: 'Javascript', visible: false, checked: false },
+ { text: 'JSON', visible: true, checked: false },
+ ]),
+ );
});
describe('when dropdown item is clicked', () => {
@@ -128,7 +188,7 @@ describe('content_editor/components/bubble_menus/code_block', () => {
findDropdownItems().at(1).vm.$emit('click');
- await Vue.nextTick();
+ await nextTick();
});
it('loads language', () => {
@@ -152,5 +212,78 @@ describe('content_editor/components/bubble_menus/code_block', () => {
expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Java');
});
});
+
+ describe('Create custom type', () => {
+ beforeEach(async () => {
+ tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
+
+ await wrapper.findComponent(GlDropdown).vm.show();
+ await wrapper.findByTestId('create-custom-type').trigger('click');
+ });
+
+ it('shows custom language input form and hides dropdown items', () => {
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(true);
+ });
+
+ describe('on clicking back', () => {
+ it('hides the custom language input form and shows dropdown items', async () => {
+ await wrapper.findByRole('button', { name: 'Go back' }).trigger('click');
+
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false);
+ });
+ });
+
+ describe('on clicking cancel', () => {
+ it('hides the custom language input form and shows dropdown items', async () => {
+ await wrapper.findByRole('button', { name: 'Cancel' }).trigger('click');
+
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false);
+ });
+ });
+
+ describe('on dropdown hide', () => {
+ it('hides the form', async () => {
+ wrapper.findComponent(GlFormInput).setValue('foobar');
+ await wrapper.findComponent(GlDropdown).vm.$emit('hide');
+
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false);
+ });
+ });
+
+ describe('on clicking apply', () => {
+ beforeEach(async () => {
+ wrapper.findComponent(GlFormInput).setValue('foobar');
+ await wrapper.findComponent(GlDropdownForm).vm.$emit('submit', createFakeEvent());
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ });
+
+ it('hides the custom language input form and shows dropdown items', async () => {
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false);
+ });
+
+ it('updates dropdown value to the custom language type', () => {
+ expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Custom (foobar)');
+ });
+
+ it('updates tiptap editor to the custom language type', () => {
+ expect(tiptapEditor.getAttributes(CodeBlockHighlight.name)).toEqual(
+ expect.objectContaining({
+ language: 'foobar',
+ }),
+ );
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
new file mode 100644
index 00000000000..0334a18c9a1
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -0,0 +1,54 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue';
+import Diagram from '~/content_editor/extensions/diagram';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
+
+describe('content_editor/components/toolbar_more_dropdown', () => {
+ let wrapper;
+ let tiptapEditor;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({
+ extensions: [Diagram, HorizontalRule],
+ });
+ };
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = mountExtended(ToolbarMoreDropdown, {
+ provide: {
+ tiptapEditor,
+ },
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ buildEditor();
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ label | contentType | data
+ ${'Mermaid diagram'} | ${'diagram'} | ${{ language: 'mermaid' }}
+ ${'PlantUML diagram'} | ${'diagram'} | ${{ language: 'plantuml' }}
+ ${'Horizontal rule'} | ${'horizontalRule'} | ${undefined}
+ `('when option $label is clicked', ({ label, contentType, data }) => {
+ it(`inserts a ${contentType}`, async () => {
+ const commands = mockChainedCommands(tiptapEditor, ['setNode', 'focus', 'run']);
+
+ const btn = wrapper.findByRole('menuitem', { name: label });
+ await btn.trigger('click');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.setNode).toHaveBeenCalledWith(contentType, data);
+ expect(commands.run).toHaveBeenCalled();
+
+ expect(wrapper.emitted('execute')).toEqual([[{ contentType }]]);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index ec58877470c..d98a9a52aff 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -23,20 +23,21 @@ describe('content_editor/components/top_toolbar', () => {
});
describe.each`
- testId | controlProps
- ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
- ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
- ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
- ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
- ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
- ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
- ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
- ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }}
- ${'horizontal-rule'} | ${{ contentType: 'horizontalRule', iconName: 'dash', label: 'Add a horizontal rule', editorCommand: 'setHorizontalRule' }}
- ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
- ${'text-styles'} | ${{}}
- ${'link'} | ${{}}
- ${'image'} | ${{}}
+ testId | controlProps
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
+ ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
+ ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
+ ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
+ ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }}
+ ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
+ ${'text-styles'} | ${{}}
+ ${'link'} | ${{}}
+ ${'image'} | ${{}}
+ ${'table'} | ${{}}
+ ${'more'} | ${{}}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
index a564959a3a6..17a365e12bb 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -1,8 +1,14 @@
import { nextTick } from 'vue';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
-import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import eventHubFactory from '~/helpers/event_hub_factory';
+import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Diagram from '~/content_editor/extensions/diagram';
import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
+import { emitEditorEvent, createTestEditor } from '../../test_utils';
jest.mock('~/content_editor/services/code_block_language_loader');
@@ -10,22 +16,43 @@ describe('content/components/wrappers/code_block', () => {
const language = 'yaml';
let wrapper;
let updateAttributesFn;
+ let tiptapEditor;
+ let contentEditor;
+ let eventHub;
+
+ const buildEditor = () => {
+ tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] });
+ contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') };
+ eventHub = eventHubFactory();
+ };
const createWrapper = async (nodeAttrs = { language }) => {
updateAttributesFn = jest.fn();
- wrapper = shallowMount(CodeBlockWrapper, {
+ wrapper = mountExtended(CodeBlockWrapper, {
propsData: {
+ editor: tiptapEditor,
node: {
attrs: nodeAttrs,
},
updateAttributes: updateAttributesFn,
},
+ stubs: {
+ NodeViewContent: stubComponent(NodeViewContent),
+ NodeViewWrapper: stubComponent(NodeViewWrapper),
+ },
+ provide: {
+ contentEditor,
+ tiptapEditor,
+ eventHub,
+ },
});
};
beforeEach(() => {
- codeBlockLanguageLoader.findLanguageBySyntax.mockReturnValue({ syntax: language });
+ buildEditor();
+
+ codeBlockLanguageLoader.findOrCreateLanguageBySyntax.mockReturnValue({ syntax: language });
});
afterEach(() => {
@@ -68,4 +95,56 @@ describe('content/components/wrappers/code_block', () => {
expect(updateAttributesFn).toHaveBeenCalledWith({ language });
});
+
+ describe('diagrams', () => {
+ beforeEach(() => {
+ jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(true);
+ });
+
+ it('does not render a preview if showPreview: false', async () => {
+ createWrapper({ language: 'plantuml', isDiagram: true, showPreview: false });
+
+ expect(wrapper.find({ ref: 'diagramContainer' }).exists()).toBe(false);
+ });
+
+ it('does not update preview when diagram is not active', async () => {
+ createWrapper({ language: 'plantuml', isDiagram: true, showPreview: true });
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ await nextTick();
+
+ expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram');
+
+ jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(false);
+
+ const alternateUrl = 'url/to/another/diagram';
+
+ contentEditor.renderDiagram.mockResolvedValue(alternateUrl);
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ await nextTick();
+
+ expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram');
+ });
+
+ it('renders an image with preview for a plantuml/kroki diagram', async () => {
+ createWrapper({ language: 'plantuml', isDiagram: true, showPreview: true });
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ await nextTick();
+
+ expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram');
+ expect(wrapper.find(SandboxedMermaid).exists()).toBe(false);
+ });
+
+ it('renders an iframe with preview for a mermaid diagram', async () => {
+ createWrapper({ language: 'mermaid', isDiagram: true, showPreview: true });
+
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ await nextTick();
+
+ expect(wrapper.find(SandboxedMermaid).props('source')).toBe('');
+ expect(wrapper.find('img').exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
new file mode 100644
index 00000000000..1ff750eb2ac
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import FootnoteDefinitionWrapper from '~/content_editor/components/wrappers/footnote_definition.vue';
+
+describe('content/components/wrappers/footnote_definition', () => {
+ let wrapper;
+
+ const createWrapper = async (node = {}) => {
+ wrapper = shallowMountExtended(FootnoteDefinitionWrapper, {
+ propsData: {
+ node,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders footnote label as a readyonly element', () => {
+ const label = 'footnote';
+
+ createWrapper({
+ attrs: {
+ label,
+ },
+ });
+ expect(wrapper.text()).toContain(label);
+ expect(wrapper.findByTestId('footnote-label').attributes().contenteditable).toBe('false');
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/footnote_definition_spec.js b/spec/frontend/content_editor/extensions/footnote_definition_spec.js
new file mode 100644
index 00000000000..d3dbc56ae0e
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/footnote_definition_spec.js
@@ -0,0 +1,7 @@
+import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
+
+describe('content_editor/extensions/footnote_definition', () => {
+ it('sets the isolation option to true', () => {
+ expect(FootnoteDefinition.config.isolating).toBe(true);
+ });
+});
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index 6348b97d918..60dc540e192 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -3,6 +3,8 @@ import Blockquote from '~/content_editor/extensions/blockquote';
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 FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
+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';
@@ -11,11 +13,19 @@ 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 Paragraph from '~/content_editor/extensions/paragraph';
import Sourcemap from '~/content_editor/extensions/sourcemap';
+import Strike from '~/content_editor/extensions/strike';
+import Table from '~/content_editor/extensions/table';
+import TableHeader from '~/content_editor/extensions/table_header';
+import TableRow from '~/content_editor/extensions/table_row';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TaskList from '~/content_editor/extensions/task_list';
+import TaskItem from '~/content_editor/extensions/task_item';
import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
-import { createTestEditor } from './test_utils';
+import { createTestEditor, createDocBuilder } from './test_utils';
const tiptapEditor = createTestEditor({
extensions: [
@@ -24,6 +34,8 @@ const tiptapEditor = createTestEditor({
BulletList,
Code,
CodeBlockHighlight,
+ FootnoteDefinition,
+ FootnoteReference,
HardBreak,
Heading,
HorizontalRule,
@@ -33,9 +45,72 @@ const tiptapEditor = createTestEditor({
ListItem,
OrderedList,
Sourcemap,
+ Strike,
+ Table,
+ TableRow,
+ TableHeader,
+ TableCell,
+ TaskList,
+ TaskItem,
],
});
+const {
+ builders: {
+ doc,
+ paragraph,
+ bold,
+ blockquote,
+ bulletList,
+ code,
+ codeBlock,
+ footnoteDefinition,
+ footnoteReference,
+ hardBreak,
+ heading,
+ horizontalRule,
+ image,
+ italic,
+ link,
+ listItem,
+ orderedList,
+ strike,
+ table,
+ tableRow,
+ tableHeader,
+ tableCell,
+ taskItem,
+ taskList,
+ },
+} = createDocBuilder({
+ tiptapEditor,
+ names: {
+ blockquote: { nodeType: Blockquote.name },
+ bold: { markType: Bold.name },
+ bulletList: { nodeType: BulletList.name },
+ code: { markType: Code.name },
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ footnoteDefinition: { nodeType: FootnoteDefinition.name },
+ footnoteReference: { nodeType: FootnoteReference.name },
+ hardBreak: { nodeType: HardBreak.name },
+ heading: { nodeType: Heading.name },
+ horizontalRule: { nodeType: HorizontalRule.name },
+ image: { nodeType: Image.name },
+ italic: { nodeType: Italic.name },
+ link: { markType: Link.name },
+ listItem: { nodeType: ListItem.name },
+ orderedList: { nodeType: OrderedList.name },
+ paragraph: { nodeType: Paragraph.name },
+ strike: { nodeType: Strike.name },
+ table: { nodeType: Table.name },
+ tableCell: { nodeType: TableCell.name },
+ tableHeader: { nodeType: TableHeader.name },
+ tableRow: { nodeType: TableRow.name },
+ taskItem: { nodeType: TaskItem.name },
+ taskList: { nodeType: TaskList.name },
+ },
+});
+
describe('Client side Markdown processing', () => {
const deserialize = async (content) => {
const { document } = await remarkMarkdownDeserializer().deserialize({
@@ -52,197 +127,887 @@ describe('Client side Markdown processing', () => {
pristineDoc: document,
});
- it.each([
+ const sourceAttrs = (sourceMapKey, sourceMarkdown) => ({
+ sourceMapKey,
+ sourceMarkdown,
+ });
+
+ const examples = [
{
markdown: '__bold text__',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:13', '__bold text__'),
+ bold(sourceAttrs('0:13', '__bold text__'), 'bold text'),
+ ),
+ ),
},
{
markdown: '**bold text**',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:13', '**bold text**'),
+ bold(sourceAttrs('0:13', '**bold text**'), 'bold text'),
+ ),
+ ),
},
{
markdown: '<strong>bold text</strong>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:26', '<strong>bold text</strong>'),
+ bold(sourceAttrs('0:26', '<strong>bold text</strong>'), 'bold text'),
+ ),
+ ),
},
{
markdown: '<b>bold text</b>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:16', '<b>bold text</b>'),
+ bold(sourceAttrs('0:16', '<b>bold text</b>'), 'bold text'),
+ ),
+ ),
},
{
markdown: '_italic text_',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:13', '_italic text_'),
+ italic(sourceAttrs('0:13', '_italic text_'), 'italic text'),
+ ),
+ ),
},
{
markdown: '*italic text*',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:13', '*italic text*'),
+ italic(sourceAttrs('0:13', '*italic text*'), 'italic text'),
+ ),
+ ),
},
{
markdown: '<em>italic text</em>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:20', '<em>italic text</em>'),
+ italic(sourceAttrs('0:20', '<em>italic text</em>'), 'italic text'),
+ ),
+ ),
},
{
markdown: '<i>italic text</i>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:18', '<i>italic text</i>'),
+ italic(sourceAttrs('0:18', '<i>italic text</i>'), 'italic text'),
+ ),
+ ),
},
{
markdown: '`inline code`',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:13', '`inline code`'),
+ code(sourceAttrs('0:13', '`inline code`'), 'inline code'),
+ ),
+ ),
},
{
markdown: '**`inline code bold`**',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:22', '**`inline code bold`**'),
+ bold(
+ sourceAttrs('0:22', '**`inline code bold`**'),
+ code(sourceAttrs('2:20', '`inline code bold`'), 'inline code bold'),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: '_`inline code italics`_',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:23', '_`inline code italics`_'),
+ italic(
+ sourceAttrs('0:23', '_`inline code italics`_'),
+ code(sourceAttrs('1:22', '`inline code italics`'), 'inline code italics'),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: `
+<i class="foo">
+ *bar*
+</i>
+ `,
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'),
+ italic(sourceAttrs('0:28', '<i class="foo">\n *bar*\n</i>'), '\n *bar*\n'),
+ ),
+ ),
+ },
+ {
+ markdown: `
+
+<img src="bar" alt="foo" />
+
+ `,
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:27', '<img src="bar" alt="foo" />'),
+ image({ ...sourceAttrs('0:27', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
+ ),
+ ),
},
{
- markdown: '__`inline code italics`__',
+ markdown: `
+- List item 1
+
+<img src="bar" alt="foo" />
+
+ `,
+ expectedDoc: doc(
+ bulletList(
+ sourceAttrs('0:13', '- List item 1'),
+ listItem(
+ sourceAttrs('0:13', '- List item 1'),
+ paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
+ ),
+ ),
+ paragraph(
+ sourceAttrs('15:42', '<img src="bar" alt="foo" />'),
+ image({ ...sourceAttrs('15:42', '<img src="bar" alt="foo" />'), alt: 'foo', src: 'bar' }),
+ ),
+ ),
},
{
markdown: '[GitLab](https://gitlab.com "Go to GitLab")',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'),
+ link(
+ {
+ ...sourceAttrs('0:43', '[GitLab](https://gitlab.com "Go to GitLab")'),
+ href: 'https://gitlab.com',
+ title: 'Go to GitLab',
+ },
+ 'GitLab',
+ ),
+ ),
+ ),
},
{
markdown: '**[GitLab](https://gitlab.com "Go to GitLab")**',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'),
+ bold(
+ sourceAttrs('0:47', '**[GitLab](https://gitlab.com "Go to GitLab")**'),
+ link(
+ {
+ ...sourceAttrs('2:45', '[GitLab](https://gitlab.com "Go to GitLab")'),
+ href: 'https://gitlab.com',
+ title: 'Go to GitLab',
+ },
+ 'GitLab',
+ ),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: 'www.commonmark.org',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:18', 'www.commonmark.org'),
+ link(
+ {
+ ...sourceAttrs('0:18', 'www.commonmark.org'),
+ href: 'http://www.commonmark.org',
+ },
+ 'www.commonmark.org',
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: 'Visit www.commonmark.org/help for more information.',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:51', 'Visit www.commonmark.org/help for more information.'),
+ 'Visit ',
+ link(
+ {
+ ...sourceAttrs('6:29', 'www.commonmark.org/help'),
+ href: 'http://www.commonmark.org/help',
+ },
+ 'www.commonmark.org/help',
+ ),
+ ' for more information.',
+ ),
+ ),
+ },
+ {
+ markdown: 'hello@mail+xyz.example isn’t valid, but hello+xyz@mail.example is.',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:66', 'hello@mail+xyz.example isn’t valid, but hello+xyz@mail.example is.'),
+ 'hello@mail+xyz.example isn’t valid, but ',
+ link(
+ {
+ ...sourceAttrs('40:62', 'hello+xyz@mail.example'),
+ href: 'mailto:hello+xyz@mail.example',
+ },
+ 'hello+xyz@mail.example',
+ ),
+ ' is.',
+ ),
+ ),
+ },
+ {
+ markdown: '[https://gitlab.com>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:20', '[https://gitlab.com>'),
+ '[',
+ link(
+ {
+ ...sourceAttrs(),
+ href: 'https://gitlab.com',
+ },
+ 'https://gitlab.com',
+ ),
+ '>',
+ ),
+ ),
},
{
markdown: `
This is a paragraph with a\\
hard line break`,
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:43', 'This is a paragraph with a\\\nhard line break'),
+ 'This is a paragraph with a',
+ hardBreak(sourceAttrs('26:28', '\\\n')),
+ '\nhard line break',
+ ),
+ ),
},
{
markdown: '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:57', '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
+ image({
+ ...sourceAttrs('0:57', '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")'),
+ alt: 'GitLab Logo',
+ src: 'https://gitlab.com/logo.png',
+ title: 'GitLab Logo',
+ }),
+ ),
+ ),
},
{
markdown: '---',
+ expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '---'))),
},
{
markdown: '***',
+ expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '***'))),
},
{
markdown: '___',
+ expectedDoc: doc(horizontalRule(sourceAttrs('0:3', '___'))),
},
{
markdown: '<hr>',
+ expectedDoc: doc(horizontalRule(sourceAttrs('0:4', '<hr>'))),
},
{
markdown: '# Heading 1',
+ expectedDoc: doc(heading({ ...sourceAttrs('0:11', '# Heading 1'), level: 1 }, 'Heading 1')),
},
{
markdown: '## Heading 2',
+ expectedDoc: doc(heading({ ...sourceAttrs('0:12', '## Heading 2'), level: 2 }, 'Heading 2')),
},
{
markdown: '### Heading 3',
+ expectedDoc: doc(heading({ ...sourceAttrs('0:13', '### Heading 3'), level: 3 }, 'Heading 3')),
},
{
markdown: '#### Heading 4',
+ expectedDoc: doc(
+ heading({ ...sourceAttrs('0:14', '#### Heading 4'), level: 4 }, 'Heading 4'),
+ ),
},
{
markdown: '##### Heading 5',
+ expectedDoc: doc(
+ heading({ ...sourceAttrs('0:15', '##### Heading 5'), level: 5 }, 'Heading 5'),
+ ),
},
{
markdown: '###### Heading 6',
+ expectedDoc: doc(
+ heading({ ...sourceAttrs('0:16', '###### Heading 6'), level: 6 }, 'Heading 6'),
+ ),
},
-
{
markdown: `
- Heading
- one
- ======
- `,
+Heading
+one
+======
+ `,
+ expectedDoc: doc(
+ heading({ ...sourceAttrs('0:18', 'Heading\none\n======'), level: 1 }, 'Heading\none'),
+ ),
},
{
markdown: `
- Heading
- two
- -------
- `,
+Heading
+two
+-------
+ `,
+ expectedDoc: doc(
+ heading({ ...sourceAttrs('0:19', 'Heading\ntwo\n-------'), level: 2 }, 'Heading\ntwo'),
+ ),
},
{
markdown: `
- - List item 1
- - List item 2
- `,
+- List item 1
+- List item 2
+ `,
+ expectedDoc: doc(
+ bulletList(
+ sourceAttrs('0:27', '- List item 1\n- List item 2'),
+ listItem(
+ sourceAttrs('0:13', '- List item 1'),
+ paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
+ ),
+ listItem(
+ sourceAttrs('14:27', '- List item 2'),
+ paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
},
{
markdown: `
- * List item 1
- * List item 2
- `,
+* List item 1
+* List item 2
+ `,
+ expectedDoc: doc(
+ bulletList(
+ sourceAttrs('0:27', '* List item 1\n* List item 2'),
+ listItem(
+ sourceAttrs('0:13', '* List item 1'),
+ paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
+ ),
+ listItem(
+ sourceAttrs('14:27', '* List item 2'),
+ paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
},
{
markdown: `
- + List item 1
- + List item 2
- `,
++ List item 1
++ List item 2
+ `,
+ expectedDoc: doc(
+ bulletList(
+ sourceAttrs('0:27', '+ List item 1\n+ List item 2'),
+ listItem(
+ sourceAttrs('0:13', '+ List item 1'),
+ paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
+ ),
+ listItem(
+ sourceAttrs('14:27', '+ List item 2'),
+ paragraph(sourceAttrs('16:27', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
},
{
markdown: `
- 1. List item 1
- 1. List item 2
- `,
+1. List item 1
+1. List item 2
+ `,
+ expectedDoc: doc(
+ orderedList(
+ sourceAttrs('0:29', '1. List item 1\n1. List item 2'),
+ listItem(
+ sourceAttrs('0:14', '1. List item 1'),
+ paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
+ ),
+ listItem(
+ sourceAttrs('15:29', '1. List item 2'),
+ paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
},
{
markdown: `
- 1. List item 1
- 2. List item 2
- `,
+1. List item 1
+2. List item 2
+ `,
+ expectedDoc: doc(
+ orderedList(
+ sourceAttrs('0:29', '1. List item 1\n2. List item 2'),
+ listItem(
+ sourceAttrs('0:14', '1. List item 1'),
+ paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
+ ),
+ listItem(
+ sourceAttrs('15:29', '2. List item 2'),
+ paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
},
{
markdown: `
- 1) List item 1
- 2) List item 2
- `,
+1) List item 1
+2) List item 2
+ `,
+ expectedDoc: doc(
+ orderedList(
+ sourceAttrs('0:29', '1) List item 1\n2) List item 2'),
+ listItem(
+ sourceAttrs('0:14', '1) List item 1'),
+ paragraph(sourceAttrs('3:14', 'List item 1'), 'List item 1'),
+ ),
+ listItem(
+ sourceAttrs('15:29', '2) List item 2'),
+ paragraph(sourceAttrs('18:29', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
},
{
markdown: `
- - List item 1
- - Sub list item 1
- `,
+- List item 1
+ - Sub list item 1
+ `,
+ expectedDoc: doc(
+ bulletList(
+ sourceAttrs('0:33', '- List item 1\n - Sub list item 1'),
+ listItem(
+ sourceAttrs('0:33', '- List item 1\n - Sub list item 1'),
+ paragraph(sourceAttrs('2:13', 'List item 1'), 'List item 1'),
+ bulletList(
+ sourceAttrs('16:33', '- Sub list item 1'),
+ listItem(
+ sourceAttrs('16:33', '- Sub list item 1'),
+ paragraph(sourceAttrs('18:33', 'Sub list item 1'), 'Sub list item 1'),
+ ),
+ ),
+ ),
+ ),
+ ),
},
{
markdown: `
- - List item 1 paragraph 1
+- List item 1 paragraph 1
- List item 1 paragraph 2
- - List item 2
- `,
+ List item 1 paragraph 2
+- List item 2
+ `,
+ expectedDoc: doc(
+ bulletList(
+ sourceAttrs(
+ '0:66',
+ '- List item 1 paragraph 1\n\n List item 1 paragraph 2\n- List item 2',
+ ),
+ listItem(
+ sourceAttrs('0:52', '- List item 1 paragraph 1\n\n List item 1 paragraph 2'),
+ paragraph(sourceAttrs('2:25', 'List item 1 paragraph 1'), 'List item 1 paragraph 1'),
+ paragraph(sourceAttrs('29:52', 'List item 1 paragraph 2'), 'List item 1 paragraph 2'),
+ ),
+ listItem(
+ sourceAttrs('53:66', '- List item 2'),
+ paragraph(sourceAttrs('55:66', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
},
{
markdown: `
- > This is a blockquote
- `,
+- List item with an image ![bar](foo.png)
+`,
+ expectedDoc: doc(
+ bulletList(
+ sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'),
+ listItem(
+ sourceAttrs('0:41', '- List item with an image ![bar](foo.png)'),
+ paragraph(
+ sourceAttrs('2:41', 'List item with an image ![bar](foo.png)'),
+ 'List item with an image',
+ image({ ...sourceAttrs('26:41', '![bar](foo.png)'), alt: 'bar', src: 'foo.png' }),
+ ),
+ ),
+ ),
+ ),
},
{
markdown: `
- > - List item 1
- > - List item 2
- `,
+> This is a blockquote
+ `,
+ expectedDoc: doc(
+ blockquote(
+ sourceAttrs('0:22', '> This is a blockquote'),
+ paragraph(sourceAttrs('2:22', 'This is a blockquote'), 'This is a blockquote'),
+ ),
+ ),
},
{
markdown: `
- const fn = () => 'GitLab';
- `,
+> - List item 1
+> - List item 2
+ `,
+ expectedDoc: doc(
+ blockquote(
+ sourceAttrs('0:31', '> - List item 1\n> - List item 2'),
+ bulletList(
+ sourceAttrs('2:31', '- List item 1\n> - List item 2'),
+ listItem(
+ sourceAttrs('2:15', '- List item 1'),
+ paragraph(sourceAttrs('4:15', 'List item 1'), 'List item 1'),
+ ),
+ listItem(
+ sourceAttrs('18:31', '- List item 2'),
+ paragraph(sourceAttrs('20:31', 'List item 2'), 'List item 2'),
+ ),
+ ),
+ ),
+ ),
},
{
markdown: `
- \`\`\`javascript
- const fn = () => 'GitLab';
- \`\`\`\
- `,
+code block
+
+ const fn = () => 'GitLab';
+
+ `,
+ expectedDoc: doc(
+ paragraph(sourceAttrs('0:10', 'code block'), 'code block'),
+ codeBlock(
+ {
+ ...sourceAttrs('12:42', " const fn = () => 'GitLab';"),
+ class: 'code highlight',
+ language: null,
+ },
+ "const fn = () => 'GitLab';",
+ ),
+ ),
},
{
markdown: `
- ~~~javascript
- const fn = () => 'GitLab';
- ~~~
- `,
+\`\`\`javascript
+const fn = () => 'GitLab';
+\`\`\`\
+ `,
+ expectedDoc: doc(
+ codeBlock(
+ {
+ ...sourceAttrs('0:44', "```javascript\nconst fn = () => 'GitLab';\n```"),
+ class: 'code highlight',
+ language: 'javascript',
+ },
+ "const fn = () => 'GitLab';",
+ ),
+ ),
},
{
markdown: `
- \`\`\`
- \`\`\`\
- `,
+~~~javascript
+const fn = () => 'GitLab';
+~~~
+ `,
+ expectedDoc: doc(
+ codeBlock(
+ {
+ ...sourceAttrs('0:44', "~~~javascript\nconst fn = () => 'GitLab';\n~~~"),
+ class: 'code highlight',
+ language: 'javascript',
+ },
+ "const fn = () => 'GitLab';",
+ ),
+ ),
},
{
markdown: `
- \`\`\`javascript
- const fn = () => 'GitLab';
+\`\`\`
+\`\`\`\
+ `,
+ expectedDoc: doc(
+ codeBlock(
+ {
+ ...sourceAttrs('0:7', '```\n```'),
+ class: 'code highlight',
+ language: null,
+ },
+ '',
+ ),
+ ),
+ },
+ {
+ markdown: `
+\`\`\`javascript
+const fn = () => 'GitLab';
- \`\`\`\
- `,
+\`\`\`\
+ `,
+ expectedDoc: doc(
+ codeBlock(
+ {
+ ...sourceAttrs('0:45', "```javascript\nconst fn = () => 'GitLab';\n\n```"),
+ class: 'code highlight',
+ language: 'javascript',
+ },
+ "const fn = () => 'GitLab';\n",
+ ),
+ ),
+ },
+ {
+ markdown: '~~Strikedthrough text~~',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:23', '~~Strikedthrough text~~'),
+ strike(sourceAttrs('0:23', '~~Strikedthrough text~~'), 'Strikedthrough text'),
+ ),
+ ),
+ },
+ {
+ markdown: '<del>Strikedthrough text</del>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:30', '<del>Strikedthrough text</del>'),
+ strike(sourceAttrs('0:30', '<del>Strikedthrough text</del>'), 'Strikedthrough text'),
+ ),
+ ),
+ },
+ {
+ markdown: '<strike>Strikedthrough text</strike>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'),
+ strike(
+ sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'),
+ 'Strikedthrough text',
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: '<s>Strikedthrough text</s>',
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:26', '<s>Strikedthrough text</s>'),
+ strike(sourceAttrs('0:26', '<s>Strikedthrough text</s>'), 'Strikedthrough text'),
+ ),
+ ),
},
- ])('processes %s correctly', async ({ markdown }) => {
+ {
+ markdown: `
+- [ ] task list item 1
+- [ ] task list item 2
+ `,
+ expectedDoc: doc(
+ taskList(
+ {
+ numeric: false,
+ ...sourceAttrs('0:45', '- [ ] task list item 1\n- [ ] task list item 2'),
+ },
+ taskItem(
+ {
+ checked: false,
+ ...sourceAttrs('0:22', '- [ ] task list item 1'),
+ },
+ paragraph(sourceAttrs('6:22', 'task list item 1'), 'task list item 1'),
+ ),
+ taskItem(
+ {
+ checked: false,
+ ...sourceAttrs('23:45', '- [ ] task list item 2'),
+ },
+ paragraph(sourceAttrs('29:45', 'task list item 2'), 'task list item 2'),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: `
+- [x] task list item 1
+- [x] task list item 2
+ `,
+ expectedDoc: doc(
+ taskList(
+ {
+ numeric: false,
+ ...sourceAttrs('0:45', '- [x] task list item 1\n- [x] task list item 2'),
+ },
+ taskItem(
+ {
+ checked: true,
+ ...sourceAttrs('0:22', '- [x] task list item 1'),
+ },
+ paragraph(sourceAttrs('6:22', 'task list item 1'), 'task list item 1'),
+ ),
+ taskItem(
+ {
+ checked: true,
+ ...sourceAttrs('23:45', '- [x] task list item 2'),
+ },
+ paragraph(sourceAttrs('29:45', 'task list item 2'), 'task list item 2'),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: `
+1. [ ] task list item 1
+2. [ ] task list item 2
+ `,
+ expectedDoc: doc(
+ taskList(
+ {
+ numeric: true,
+ ...sourceAttrs('0:47', '1. [ ] task list item 1\n2. [ ] task list item 2'),
+ },
+ taskItem(
+ {
+ checked: false,
+ ...sourceAttrs('0:23', '1. [ ] task list item 1'),
+ },
+ paragraph(sourceAttrs('7:23', 'task list item 1'), 'task list item 1'),
+ ),
+ taskItem(
+ {
+ checked: false,
+ ...sourceAttrs('24:47', '2. [ ] task list item 2'),
+ },
+ paragraph(sourceAttrs('31:47', 'task list item 2'), 'task list item 2'),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: `
+| a | b |
+|---|---|
+| c | d |
+`,
+ expectedDoc: doc(
+ table(
+ sourceAttrs('0:29', '| a | b |\n|---|---|\n| c | d |'),
+ tableRow(
+ sourceAttrs('0:9', '| a | b |'),
+ tableHeader(sourceAttrs('0:5', '| a |'), paragraph(sourceAttrs('2:3', 'a'), 'a')),
+ tableHeader(sourceAttrs('5:9', ' b |'), paragraph(sourceAttrs('6:7', 'b'), 'b')),
+ ),
+ tableRow(
+ sourceAttrs('20:29', '| c | d |'),
+ tableCell(sourceAttrs('20:25', '| c |'), paragraph(sourceAttrs('22:23', 'c'), 'c')),
+ tableCell(sourceAttrs('25:29', ' d |'), paragraph(sourceAttrs('26:27', 'd'), 'd')),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: `
+<table>
+ <tr>
+ <th colspan="2" rowspan="5">Header</th>
+ </tr>
+ <tr>
+ <td colspan="2" rowspan="5">Body</td>
+ </tr>
+</table>
+`,
+ expectedDoc: doc(
+ table(
+ sourceAttrs(
+ '0:132',
+ '<table>\n <tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>\n <tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>\n</table>',
+ ),
+ tableRow(
+ sourceAttrs('10:66', '<tr>\n <th colspan="2" rowspan="5">Header</th>\n </tr>'),
+ tableHeader(
+ {
+ ...sourceAttrs('19:58', '<th colspan="2" rowspan="5">Header</th>'),
+ colspan: 2,
+ rowspan: 5,
+ },
+ paragraph(sourceAttrs('47:53', 'Header'), 'Header'),
+ ),
+ ),
+ tableRow(
+ sourceAttrs('69:123', '<tr>\n <td colspan="2" rowspan="5">Body</td>\n </tr>'),
+ tableCell(
+ {
+ ...sourceAttrs('78:115', '<td colspan="2" rowspan="5">Body</td>'),
+ colspan: 2,
+ rowspan: 5,
+ },
+ paragraph(sourceAttrs('106:110', 'Body'), 'Body'),
+ ),
+ ),
+ ),
+ ),
+ },
+ {
+ markdown: `
+This is a footnote [^footnote]
+
+Paragraph
+
+[^footnote]: Footnote definition
+
+Paragraph
+`,
+ expectedDoc: doc(
+ paragraph(
+ sourceAttrs('0:30', 'This is a footnote [^footnote]'),
+ 'This is a footnote ',
+ footnoteReference({
+ ...sourceAttrs('19:30', '[^footnote]'),
+ identifier: 'footnote',
+ label: 'footnote',
+ }),
+ ),
+ paragraph(sourceAttrs('32:41', 'Paragraph'), 'Paragraph'),
+ footnoteDefinition(
+ {
+ ...sourceAttrs('43:75', '[^footnote]: Footnote definition'),
+ identifier: 'footnote',
+ label: 'footnote',
+ },
+ paragraph(sourceAttrs('56:75', 'Footnote definition'), 'Footnote definition'),
+ ),
+ paragraph(sourceAttrs('77:86', 'Paragraph'), 'Paragraph'),
+ ),
+ },
+ ];
+
+ const runOnly = examples.find((example) => example.only === true);
+ const runExamples = runOnly ? [runOnly] : examples;
+
+ it.each(runExamples)('processes %s correctly', async ({ markdown, expectedDoc }) => {
const trimmed = markdown.trim();
const document = await deserialize(trimmed);
+ expect(expectedDoc).not.toBeFalsy();
+ expect(document.toJSON()).toEqual(expectedDoc.toJSON());
expect(serialize(document)).toEqual(trimmed);
});
});
diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js
index f4e7d9bf881..0a99f823be3 100644
--- a/spec/frontend/content_editor/services/asset_resolver_spec.js
+++ b/spec/frontend/content_editor/services/asset_resolver_spec.js
@@ -20,4 +20,14 @@ describe('content_editor/services/asset_resolver', () => {
);
});
});
+
+ describe('renderDiagram', () => {
+ it('resolves a diagram code to a url containing the diagram image', async () => {
+ renderMarkdown.mockResolvedValue(
+ '<p><img data-diagram="nomnoml" src="url/to/some/diagram"></p>',
+ );
+
+ expect(await assetResolver.renderDiagram('test')).toBe('url/to/some/diagram');
+ });
+ });
});
diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
index 943de327762..795f5219a3f 100644
--- a/spec/frontend/content_editor/services/code_block_language_loader_spec.js
+++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
@@ -18,25 +18,32 @@ describe('content_editor/services/code_block_language_loader', () => {
languageLoader.lowlight = lowlight;
});
- describe('findLanguageBySyntax', () => {
+ describe('findOrCreateLanguageBySyntax', () => {
it.each`
syntax | language
${'javascript'} | ${{ syntax: 'javascript', label: 'Javascript' }}
${'js'} | ${{ syntax: 'javascript', label: 'Javascript' }}
${'jsx'} | ${{ syntax: 'javascript', label: 'Javascript' }}
`('returns a language by syntax and its variants', ({ syntax, language }) => {
- expect(languageLoader.findLanguageBySyntax(syntax)).toMatchObject(language);
+ expect(languageLoader.findOrCreateLanguageBySyntax(syntax)).toMatchObject(language);
});
it('returns Custom (syntax) if the language does not exist', () => {
- expect(languageLoader.findLanguageBySyntax('foobar')).toMatchObject({
+ expect(languageLoader.findOrCreateLanguageBySyntax('foobar')).toMatchObject({
syntax: 'foobar',
label: 'Custom (foobar)',
});
});
+ it('returns Diagram (syntax) if the language does not exist, and isDiagram = true', () => {
+ expect(languageLoader.findOrCreateLanguageBySyntax('foobar', true)).toMatchObject({
+ syntax: 'foobar',
+ label: 'Diagram (foobar)',
+ });
+ });
+
it('returns plaintext if no syntax is passed', () => {
- expect(languageLoader.findLanguageBySyntax('')).toMatchObject({
+ expect(languageLoader.findOrCreateLanguageBySyntax('')).toMatchObject({
syntax: 'plaintext',
label: 'Plain text',
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 25b7483f234..13e9efaea59 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -13,7 +13,6 @@ 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 HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
@@ -53,7 +52,6 @@ const tiptapEditor = createTestEditor({
Emoji,
FootnoteDefinition,
FootnoteReference,
- FootnotesSection,
Figure,
FigureCaption,
HardBreak,
@@ -92,7 +90,6 @@ const {
emoji,
footnoteDefinition,
footnoteReference,
- footnotesSection,
figure,
figureCaption,
heading,
@@ -131,7 +128,6 @@ const {
figureCaption: { nodeType: FigureCaption.name },
footnoteDefinition: { nodeType: FootnoteDefinition.name },
footnoteReference: { nodeType: FootnoteReference.name },
- footnotesSection: { nodeType: FootnotesSection.name },
hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name },
@@ -200,7 +196,7 @@ describe('markdownSerializer', () => {
it('correctly serializes a plain URL link', () => {
expect(serialize(paragraph(link({ href: 'https://example.com' }, 'https://example.com')))).toBe(
- '<https://example.com>',
+ 'https://example.com',
);
});
@@ -1147,49 +1143,75 @@ there
it('correctly serializes footnotes', () => {
expect(
serialize(
- paragraph(
- 'Oranges are orange ',
- footnoteReference({ footnoteId: '1', footnoteNumber: '1' }),
- ),
- footnotesSection(footnoteDefinition(paragraph('Oranges are fruits'))),
+ paragraph('Oranges are orange ', footnoteReference({ label: '1', identifier: '1' })),
+ footnoteDefinition({ label: '1', identifier: '1' }, 'Oranges are fruits'),
),
).toBe(
`
Oranges are orange [^1]
[^1]: Oranges are fruits
- `.trim(),
+`.trimLeft(),
);
});
+ const defaultEditAction = (initialContent) => {
+ tiptapEditor.chain().setContent(initialContent.toJSON()).insertContent(' modified').run();
+ };
+
+ const prependContentEditAction = (initialContent) => {
+ tiptapEditor
+ .chain()
+ .setContent(initialContent.toJSON())
+ .setTextSelection(0)
+ .insertContent('modified ')
+ .run();
+ };
+
it.each`
- mark | content | modifiedContent
- ${'bold'} | ${'**bold**'} | ${'**bold modified**'}
- ${'bold'} | ${'__bold__'} | ${'__bold modified__'}
- ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'}
- ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'}
- ${'italic'} | ${'_italic_'} | ${'_italic modified_'}
- ${'italic'} | ${'*italic*'} | ${'*italic modified*'}
- ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'}
- ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'}
- ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'}
- ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'}
- ${'code'} | ${'`code`'} | ${'`code modified`'}
- ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'}
+ mark | content | modifiedContent | editAction
+ ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
+ ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
+ ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
+ ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
+ ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
+ ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
+ ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
+ ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
+ ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
+ ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
+ ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction}
+ ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
+ ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
+ ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
+ ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
+ ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
+ ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
+ ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
+ ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
`(
- 'preserves original $mark syntax when sourceMarkdown is available',
- async ({ content, modifiedContent }) => {
+ 'preserves original $mark syntax when sourceMarkdown is available for $content',
+ async ({ content, modifiedContent, editAction }) => {
const { document } = await remarkMarkdownDeserializer().deserialize({
schema: tiptapEditor.schema,
content,
});
- tiptapEditor
- .chain()
- .setContent(document.toJSON())
- // changing the document ensures that block preservation doesn’t yield false positives
- .insertContent(' modified')
- .run();
+ editAction(document);
const serialized = markdownSerializer({}).serialize({
pristineDoc: document,
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
index 384d6699150..af56b94f90b 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
@@ -18,7 +18,7 @@ describe('CustomMetricsForm', () => {
wrapper = shallowMount(CustomMetricsForm, {
propsData: {
customMetricsPath: '',
- editProjectServicePath: '',
+ editIntegrationPath: '',
metricPersisted,
validateQueryPath: '',
formData,
diff --git a/spec/frontend/cycle_analytics/path_navigation_spec.js b/spec/frontend/cycle_analytics/path_navigation_spec.js
index c6d72d3b571..e8c4ebd3a38 100644
--- a/spec/frontend/cycle_analytics/path_navigation_spec.js
+++ b/spec/frontend/cycle_analytics/path_navigation_spec.js
@@ -1,4 +1,4 @@
-import { GlPath, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlPath, GlSkeletonLoader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -73,7 +73,7 @@ describe('Project PathNavigation', () => {
});
it('hides the gl-skeleton-loading component', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ expect(wrapper.find(GlSkeletonLoader).exists()).toBe(false);
});
it('renders each stage', () => {
@@ -116,7 +116,7 @@ describe('Project PathNavigation', () => {
});
it('displays the gl-skeleton-loading component', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js
index 0d15d67866d..473e1d5b664 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/cycle_analytics/stage_table_spec.js
@@ -27,6 +27,7 @@ const findTableHeadColumns = () => findTableHead().findAll('th');
const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title');
const findStageEventLink = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-link');
const findStageTime = () => wrapper.findByTestId('vsa-stage-event-time');
+const findStageLastEvent = () => wrapper.findByTestId('vsa-stage-last-event');
const findIcon = (name) => wrapper.findByTestId(`${name}-icon`);
function createComponent(props = {}, shallow = false) {
@@ -128,6 +129,10 @@ describe('StageTable', () => {
expect(findStageTime().text()).toBe(createdAt);
});
+ it('will render the end event', () => {
+ expect(findStageLastEvent().text()).toBe(firstIssueEvent.endEventTimestamp);
+ });
+
it('will render the author', () => {
expect(wrapper.findByTestId('vsa-stage-event-author').text()).toContain(
firstIssueEvent.author.name,
@@ -303,10 +308,20 @@ describe('StageTable', () => {
wrapper.destroy();
});
- it('can sort the table by each column', () => {
- findTableHeadColumns().wrappers.forEach((w) => {
- expect(w.attributes('aria-sort')).toBe('none');
- });
+ it('can sort the end event or duration', () => {
+ findTableHeadColumns()
+ .wrappers.slice(1)
+ .forEach((w) => {
+ expect(w.attributes('aria-sort')).toBe('none');
+ });
+ });
+
+ it('cannot be sorted by title', () => {
+ findTableHeadColumns()
+ .wrappers.slice(0, 1)
+ .forEach((w) => {
+ expect(w.attributes('aria-sort')).toBeUndefined();
+ });
});
it('clicking a table column will send tracking information', () => {
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index 4a3e8146b13..df86b10cba3 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -61,7 +61,7 @@ describe('ValueStreamMetrics', () => {
it('will display a loader with pending requests', async () => {
await nextTick();
- expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
describe('with data loaded', () => {
@@ -88,7 +88,7 @@ describe('ValueStreamMetrics', () => {
});
it('will not display a loading icon', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
});
describe('filterFn', () => {
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index d79dde84d46..30eddcee86a 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -36,6 +36,7 @@ describe('Design management design presentation component', () => {
discussions,
isAnnotating,
resolvedDiscussionsExpanded,
+ isLoading: false,
},
stubs,
});
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index e8426216c1c..40968d9204a 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -52,6 +52,7 @@ describe('Design management design sidebar component', () => {
design,
resolvedDiscussionsExpanded: false,
markdownPreviewPath: '',
+ isLoading: false,
...props,
},
mocks: {
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
index 3cb48d7632f..b5a69b28a88 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
@@ -18,7 +18,7 @@ exports[`Design management pagination component renders navigation buttons 1`] =
category="primary"
class="js-previous-design"
disabled="true"
- icon="angle-left"
+ icon="chevron-lg-left"
size="medium"
title="Go to previous design"
variant="default"
@@ -29,7 +29,7 @@ exports[`Design management pagination component renders navigation buttons 1`] =
buttontextclasses=""
category="primary"
class="js-next-design"
- icon="angle-right"
+ icon="chevron-lg-right"
size="medium"
title="Go to next design"
variant="default"
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
index 6dfd57906d8..3c4aa0f4d3c 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
@@ -56,7 +56,7 @@ exports[`Design management toolbar component renders design and updated data 1`]
buttonclass=""
buttonicon="archive"
buttonsize="medium"
- buttonvariant="warning"
+ buttonvariant="default"
class="gl-ml-3"
hasselecteddesigns="true"
title="Archive design"
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 243cc9d891d..be736184e60 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -45,7 +45,7 @@ exports[`Design management index page designs renders loading icon 1`] = `
<gl-loading-icon-stub
color="dark"
label="Loading"
- size="md"
+ size="lg"
/>
</div>
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 8f12dc8fb06..0f2857821ea 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -99,7 +99,7 @@ exports[`Design management design index page renders design index 1`] = `
variant="link"
>
Resolved Comments (1)
-
+
</gl-button-stub>
<gl-popover-stub
@@ -112,8 +112,8 @@ exports[`Design management design index page renders design index 1`] = `
>
<p>
- Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below
-
+ Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below
+
</p>
<a
@@ -144,19 +144,6 @@ exports[`Design management design index page renders design index 1`] = `
</div>
`;
-exports[`Design management design index page sets loading state 1`] = `
-<div
- class="design-detail js-design-detail fixed-top gl-w-full gl-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
->
- <gl-loading-icon-stub
- class="gl-align-self-center"
- color="dark"
- label="Loading"
- size="xl"
- />
-</div>
-`;
-
exports[`Design management design index page with error GlAlert is rendered in correct position with correct content 1`] = `
<div
class="design-detail js-design-detail fixed-top gl-w-full gl-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
@@ -185,8 +172,8 @@ exports[`Design management design index page with error GlAlert is rendered in c
variant="danger"
>
- woops
-
+ woops
+
</gl-alert-stub>
</div>
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 55d0fabe402..17a299c5de1 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -91,7 +91,12 @@ describe('Design management design index page', () => {
function createComponent(
{ loading = false } = {},
- { data = {}, intialRouteOptions = {}, provide = {} } = {},
+ {
+ data = {},
+ intialRouteOptions = {},
+ provide = {},
+ stubs = { ApolloMutation, DesignSidebar, DesignReplyForm },
+ } = {},
) {
const $apollo = {
queries: {
@@ -109,11 +114,7 @@ describe('Design management design index page', () => {
wrapper = shallowMount(DesignIndex, {
propsData: { id: '1' },
mocks: { $apollo },
- stubs: {
- ApolloMutation,
- DesignSidebar,
- DesignReplyForm,
- },
+ stubs,
provide: {
issueIid: '1',
projectPath: 'project-path',
@@ -139,7 +140,7 @@ describe('Design management design index page', () => {
describe('when navigating to component', () => {
it('applies fullscreen layout class', () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
- createComponent({ loading: true });
+ createComponent({}, { stubs: {} });
expect(mockPageLayoutElement.classList.add).toHaveBeenCalledTimes(1);
expect(mockPageLayoutElement.classList.add).toHaveBeenCalledWith(
@@ -151,7 +152,7 @@ describe('Design management design index page', () => {
describe('when navigating within the component', () => {
it('`scale` prop of DesignPresentation component is 1', async () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
- createComponent({ loading: false }, { data: { design, scale: 2 } });
+ createComponent({}, { data: { design, scale: 2 } });
await nextTick();
expect(findDesignPresentation().props('scale')).toBe(2);
@@ -180,7 +181,8 @@ describe('Design management design index page', () => {
it('sets loading state', () => {
createComponent({ loading: true });
- expect(wrapper.element).toMatchSnapshot();
+ expect(wrapper.find(DesignPresentation).props('isLoading')).toBe(true);
+ expect(wrapper.find(DesignSidebar).props('isLoading')).toBe(true);
});
it('renders design index', () => {
@@ -197,6 +199,7 @@ describe('Design management design index page', () => {
design,
markdownPreviewPath: '/project-path/preview_markdown?target_type=Issue',
resolvedDiscussionsExpanded: false,
+ isLoading: false,
});
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 87531e8b645..087655d10f7 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -4,6 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo, { ApolloMutation } from 'vue-apollo';
import VueRouter from 'vue-router';
+import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils';
import VueDraggable from 'vuedraggable';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -762,6 +763,25 @@ describe('Design management index page', () => {
expect(findDesigns().at(0).props('id')).toBe('2');
});
+ it.each`
+ breakpoint | reorderDisabled
+ ${'xs'} | ${true}
+ ${'sm'} | ${false}
+ ${'md'} | ${false}
+ ${'lg'} | ${false}
+ ${'xl'} | ${false}
+ `(
+ 'sets draggable disabled value to $reorderDisabled when breakpoint is $breakpoint',
+ async ({ breakpoint, reorderDisabled }) => {
+ jest.spyOn(breakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint);
+
+ createComponentWithApollo({});
+ await waitForPromises();
+
+ expect(draggableAttributes().disabled).toBe(reorderDisabled);
+ },
+ );
+
it('prevents reordering when reorderDesigns mutation is in progress', async () => {
createComponentWithApollo({});
await moveDesigns(wrapper);
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index 03ab79712a4..b9c62334223 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -20,6 +20,8 @@ function factory(routeArg) {
return mount(App, {
router,
+ provide: { issueIid: '1' },
+ stubs: { Toolbar: true },
mocks: {
$apollo: {
queries: {
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index eee17e118a0..e52c5abbc7b 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -6,8 +6,6 @@ import Component from '~/diffs/components/commit_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
-jest.mock('~/user_popovers');
-
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index bd538996349..5ff0728b358 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -1,4 +1,3 @@
-import { getByText } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
@@ -81,7 +80,7 @@ describe('DiffExpansionCell', () => {
const findExpandUp = (wrapper) => wrapper.find(EXPAND_UP_CLASS);
const findExpandDown = (wrapper) => wrapper.find(EXPAND_DOWN_CLASS);
- const findExpandAll = ({ element }) => getByText(element, 'Show all unchanged lines');
+ const findExpandAll = (wrapper) => wrapper.find('.js-unfold-all');
describe('top row', () => {
it('should have "expand up" and "show all" option', () => {
@@ -90,9 +89,7 @@ describe('DiffExpansionCell', () => {
});
expect(findExpandUp(wrapper).exists()).toBe(true);
- expect(findExpandDown(wrapper).exists()).toBe(true);
expect(findExpandUp(wrapper).attributes('disabled')).not.toBeDefined();
- expect(findExpandDown(wrapper).attributes('disabled')).toBeDefined();
expect(findExpandAll(wrapper)).not.toBe(null);
});
});
@@ -114,9 +111,7 @@ describe('DiffExpansionCell', () => {
});
expect(findExpandDown(wrapper).exists()).toBe(true);
- expect(findExpandUp(wrapper).exists()).toBe(true);
expect(findExpandDown(wrapper).attributes('disabled')).not.toBeDefined();
- expect(findExpandUp(wrapper).attributes('disabled')).toBeDefined();
expect(findExpandAll(wrapper)).not.toBe(null);
});
});
@@ -144,9 +139,9 @@ describe('DiffExpansionCell', () => {
newLineNumber,
});
- const wrapper = createComponent({ file });
+ const wrapper = createComponent({ file, lineCountBetween: 10 });
- findExpandAll(wrapper).click();
+ findExpandAll(wrapper).trigger('click');
expect(store.dispatch).toHaveBeenCalledWith(
'diffs/loadMoreLines',
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index f22bd312a6d..d90afeb6b82 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -14,7 +14,6 @@ import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
import testAction from '../../__helpers__/vuex_action_helper';
import diffDiscussionsMockData from '../mock_data/diff_discussions';
@@ -224,16 +223,6 @@ describe('DiffFileHeader component', () => {
});
expect(findFileActions().exists()).toBe(false);
});
-
- it('renders submodule icon', () => {
- createComponent({
- props: {
- diffFile: submoduleDiffFile,
- },
- });
-
- expect(wrapper.find(FileIcon).props('submodule')).toBe(true);
- });
});
describe('for any file', () => {
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index fb9dc22ce25..b59043168b8 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -64,6 +64,16 @@ describe('DiffLineNoteForm', () => {
expect(confirmAction).toHaveBeenCalled();
});
+ it('should only ask for confirmation once', () => {
+ // Never resolve so we can test what happens when triggered while "confirmAction" is loading
+ confirmAction.mockImplementation(() => new Promise(() => {}));
+
+ findNoteForm().vm.$emit('cancelForm', true, true);
+ findNoteForm().vm.$emit('cancelForm', true, true);
+
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+ });
+
it('should not ask for confirmation when one of the params false', () => {
confirmAction.mockResolvedValueOnce(false);
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index f982749d1de..dfbe30e460b 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -49,22 +49,6 @@ describe('DiffView', () => {
return shallowMount(DiffView, { propsData, store, stubs });
};
- it('renders a match line', () => {
- const wrapper = createWrapper({
- diffLines: [
- {
- isMatchLineLeft: true,
- left: {
- rich_text: '@@ -4,12 +4,12 @@ import createFlash from &#39;~/flash&#39;;',
- lineDraft: {},
- },
- },
- ],
- });
- expect(wrapper.find(DiffExpansionCell).exists()).toBe(true);
- expect(wrapper.text()).toContain("@@ -4,12 +4,12 @@ import createFlash from '~/flash';");
- });
-
it.each`
type | side | container | sides | total
${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 8ae51a58819..6f55f76d7b5 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -30,13 +30,6 @@ describe('DiffsStoreUtils', () => {
});
});
- describe('getReversePosition', () => {
- it('should return correct line position name', () => {
- expect(utils.getReversePosition(LINE_POSITION_RIGHT)).toEqual(LINE_POSITION_LEFT);
- expect(utils.getReversePosition(LINE_POSITION_LEFT)).toEqual(LINE_POSITION_RIGHT);
- });
- });
-
describe('findIndexInInlineLines', () => {
const expectSet = (method, lines, invalidLines) => {
expect(method(lines, { oldLineNumber: 3, newLineNumber: 5 })).toEqual(4);
diff --git a/spec/frontend/editor/helpers.js b/spec/frontend/editor/helpers.js
index 252d783ad6d..48d83a87a6e 100644
--- a/spec/frontend/editor/helpers.js
+++ b/spec/frontend/editor/helpers.js
@@ -49,6 +49,12 @@ export const SEConstExt = () => {
};
};
+export const SEExtWithoutAPI = () => {
+ return {
+ extensionName: 'SEExtWithoutAPI',
+ };
+};
+
export class SEWithSetupExt {
static get extensionName() {
return 'SEWithSetupExt';
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index 628c34a27c1..c59806a5d60 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -38,6 +38,7 @@ const ajv = new Ajv({
strictTuples: false,
allowMatchingProperties: true,
});
+ajv.addKeyword('markdownDescription');
AjvFormats(ajv);
const schema = ajv.compile(CiSchema);
diff --git a/spec/frontend/editor/source_editor_extension_spec.js b/spec/frontend/editor/source_editor_extension_spec.js
index c5fa795f3b7..78453aaa491 100644
--- a/spec/frontend/editor/source_editor_extension_spec.js
+++ b/spec/frontend/editor/source_editor_extension_spec.js
@@ -54,6 +54,7 @@ describe('Editor Extension', () => {
${helpers.SEClassExtension} | ${['shared', 'classExtMethod']}
${helpers.SEFnExtension} | ${['fnExtMethod']}
${helpers.SEConstExt} | ${['constExtMethod']}
+ ${helpers.SEExtWithoutAPI} | ${[]}
`('correctly returns API for $definition', ({ definition, expectedKeys }) => {
const extension = new EditorExtension({ definition });
const expectedApi = Object.fromEntries(
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 1926f3e268e..fe20c23e4d7 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -1,4 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
+import { Emitter } from 'monaco-editor';
+import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import {
@@ -64,7 +66,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
afterEach(() => {
instance.dispose();
- editorEl.remove();
mockAxios.restore();
resetHTMLFixture();
});
@@ -75,11 +76,47 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
actions: expect.any(Object),
shown: false,
modelChangeListener: undefined,
+ layoutChangeListener: {
+ dispose: expect.anything(),
+ },
path: previewMarkdownPath,
actionShowPreviewCondition: expect.any(Object),
});
});
+ describe('onDidLayoutChange', () => {
+ const emitter = new Emitter();
+ let layoutSpy;
+
+ useFakeRequestAnimationFrame();
+
+ beforeEach(() => {
+ instance.unuse(extension);
+ instance.onDidLayoutChange = emitter.event;
+ extension = instance.use({
+ definition: EditorMarkdownPreviewExtension,
+ setupOptions: { previewMarkdownPath },
+ });
+ layoutSpy = jest.spyOn(instance, 'layout');
+ });
+
+ it('does not trigger the layout when the preview is not active [default]', async () => {
+ expect(instance.markdownPreview.shown).toBe(false);
+ expect(layoutSpy).not.toHaveBeenCalled();
+ await emitter.fire();
+ expect(layoutSpy).not.toHaveBeenCalled();
+ });
+
+ it('triggers the layout if the preview panel is opened', async () => {
+ expect(layoutSpy).not.toHaveBeenCalled();
+ instance.togglePreview();
+ layoutSpy.mockReset();
+
+ await emitter.fire();
+ expect(layoutSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
describe('model change listener', () => {
let cleanupSpy;
let actionSpy;
@@ -111,6 +148,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios.onPost().reply(200, { body: responseData });
await togglePreview();
});
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
it('removes the registered buttons from the toolbar', () => {
expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
@@ -175,6 +215,31 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
instance.unuse(extension);
expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
});
+
+ it('disposes the layoutChange listener and does not re-layout on layout changes', () => {
+ expect(instance.markdownPreview.layoutChangeListener).toBeDefined();
+ instance.unuse(extension);
+
+ expect(instance.markdownPreview?.layoutChangeListener).toBeUndefined();
+ });
+
+ it('does not trigger the re-layout after instance is unused', async () => {
+ const emitter = new Emitter();
+
+ instance.unuse(extension);
+ instance.onDidLayoutChange = emitter.event;
+
+ // we have to re-use the extension to pick up the emitter
+ extension = instance.use({
+ definition: EditorMarkdownPreviewExtension,
+ setupOptions: { previewMarkdownPath },
+ });
+ instance.unuse(extension);
+ const layoutSpy = jest.spyOn(instance, 'layout');
+
+ await emitter.fire();
+ expect(layoutSpy).not.toHaveBeenCalled();
+ });
});
describe('fetchPreview', () => {
diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js
index b3d914e6755..74aae7b899b 100644
--- a/spec/frontend/editor/source_editor_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -92,7 +92,7 @@ describe('Base editor', () => {
expect(monacoEditor.createModel).toHaveBeenCalledWith(
blobContent,
- undefined,
+ 'markdown',
expect.objectContaining({
path: uriFilePath,
}),
@@ -117,7 +117,7 @@ describe('Base editor', () => {
expect(modelSpy).toHaveBeenCalledWith(
blobContent,
- undefined,
+ 'markdown',
expect.objectContaining({
path: uriFilePath,
}),
@@ -177,6 +177,29 @@ describe('Base editor', () => {
expect(layoutSpy).toHaveBeenCalled();
});
+
+ it.each`
+ params | expectedLanguage
+ ${{}} | ${'markdown'}
+ ${{ blobPath: undefined }} | ${'plaintext'}
+ ${{ blobPath: undefined, language: 'ruby' }} | ${'ruby'}
+ ${{ language: 'go' }} | ${'go'}
+ ${{ blobPath: undefined, language: undefined }} | ${'plaintext'}
+ `(
+ 'correctly sets $expectedLanguage on the model when $params are passed',
+ ({ params, expectedLanguage }) => {
+ jest.spyOn(monacoEditor, 'createModel');
+ editor.createInstance({
+ ...defaultArguments,
+ ...params,
+ });
+ expect(monacoEditor.createModel).toHaveBeenCalledWith(
+ expect.anything(),
+ expectedLanguage,
+ expect.anything(),
+ );
+ },
+ );
});
describe('instance of the Diff Editor', () => {
@@ -210,7 +233,7 @@ describe('Base editor', () => {
expect(modelSpy).toHaveBeenCalledTimes(2);
expect(modelSpy.mock.calls[0]).toEqual([
blobContent,
- undefined,
+ 'markdown',
expect.objectContaining({
path: uriFilePath,
}),
diff --git a/spec/frontend/editor/source_editor_webide_ext_spec.js b/spec/frontend/editor/source_editor_webide_ext_spec.js
new file mode 100644
index 00000000000..096b6b1646f
--- /dev/null
+++ b/spec/frontend/editor/source_editor_webide_ext_spec.js
@@ -0,0 +1,55 @@
+import { Emitter } from 'monaco-editor';
+import { setHTMLFixture } from 'helpers/fixtures';
+import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
+import SourceEditor from '~/editor/source_editor';
+
+describe('Source Editor Web IDE Extension', () => {
+ let editorEl;
+ let editor;
+ let instance;
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="editor" data-editor-loading></div>');
+ editorEl = document.getElementById('editor');
+ editor = new SourceEditor();
+ });
+ afterEach(() => {});
+
+ describe('onSetup', () => {
+ it.each`
+ width | renderSideBySide
+ ${'0'} | ${false}
+ ${'699px'} | ${false}
+ ${'700px'} | ${true}
+ `(
+ "correctly renders the Diff Editor when the parent element's width is $width",
+ ({ width, renderSideBySide }) => {
+ editorEl.style.width = width;
+ instance = editor.createDiffInstance({ el: editorEl });
+
+ const sideBySideSpy = jest.spyOn(instance, 'updateOptions');
+ instance.use({ definition: EditorWebIdeExtension });
+
+ expect(sideBySideSpy).toBeCalledWith({ renderSideBySide });
+ },
+ );
+
+ it('re-renders the Diff Editor when layout of the modified editor is changed', async () => {
+ const emitter = new Emitter();
+ editorEl.style.width = '700px';
+
+ instance = editor.createDiffInstance({ el: editorEl });
+ instance.getModifiedEditor().onDidLayoutChange = emitter.event;
+ instance.use({ definition: EditorWebIdeExtension });
+
+ const sideBySideSpy = jest.spyOn(instance, 'updateOptions');
+ await emitter.fire();
+
+ expect(sideBySideSpy).toBeCalledWith({ renderSideBySide: true });
+
+ editorEl.style.width = '0px';
+ await emitter.fire();
+ expect(sideBySideSpy).toBeCalledWith({ renderSideBySide: false });
+ });
+ });
+});
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index cc037586496..dc8f50e0e4b 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -24,6 +24,7 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
+import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
const emptySupportMap = {
personZwj: false,
@@ -436,14 +437,28 @@ describe('emoji', () => {
it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => {
const search = searchEmoji(input);
- const expected = Object.keys(validEmoji).map((name) => {
- return {
- emoji: mockEmojiData[name],
- field: 'd',
- fieldValue: mockEmojiData[name].d,
- score: 0,
- };
- });
+ const expected = Object.keys(validEmoji)
+ .map((name) => {
+ let score = NEUTRAL_INTENT_MULTIPLIER;
+
+ // Positive intent value retrieved from ~/emoji/intents.json
+ if (name === 'thumbsup') {
+ score = 0.5;
+ }
+
+ // Negative intent value retrieved from ~/emoji/intents.json
+ if (name === 'thumbsdown') {
+ score = 1.5;
+ }
+
+ return {
+ emoji: mockEmojiData[name],
+ field: 'd',
+ fieldValue: mockEmojiData[name].d,
+ score,
+ };
+ })
+ .sort(sortEmoji);
expect(search).toEqual(expected);
});
@@ -457,7 +472,7 @@ describe('emoji', () => {
name: 'atom',
field: 'e',
fieldValue: 'atom',
- score: 0,
+ score: NEUTRAL_INTENT_MULTIPLIER,
},
],
],
@@ -469,7 +484,7 @@ describe('emoji', () => {
name: 'atom',
field: 'alias',
fieldValue: 'atom_symbol',
- score: 4,
+ score: 16,
},
],
],
@@ -481,7 +496,7 @@ describe('emoji', () => {
name: 'atom',
field: 'alias',
fieldValue: 'atom_symbol',
- score: 0,
+ score: NEUTRAL_INTENT_MULTIPLIER,
},
],
],
@@ -509,7 +524,7 @@ describe('emoji', () => {
{
name: 'atom',
field: 'd',
- score: 0,
+ score: NEUTRAL_INTENT_MULTIPLIER,
},
],
],
@@ -521,7 +536,7 @@ describe('emoji', () => {
{
name: 'atom',
field: 'd',
- score: 0,
+ score: NEUTRAL_INTENT_MULTIPLIER,
},
],
],
@@ -533,7 +548,7 @@ describe('emoji', () => {
{
name: 'grey_question',
field: 'name',
- score: 5,
+ score: 32,
},
],
],
@@ -544,7 +559,7 @@ describe('emoji', () => {
{
name: 'grey_question',
field: 'd',
- score: 24,
+ score: 16777216,
},
],
],
@@ -553,14 +568,14 @@ describe('emoji', () => {
'heart',
[
{
- name: 'black_heart',
- field: 'd',
- score: 6,
- },
- {
name: 'heart',
field: 'name',
- score: 0,
+ score: NEUTRAL_INTENT_MULTIPLIER,
+ },
+ {
+ name: 'black_heart',
+ field: 'd',
+ score: 64,
},
],
],
@@ -569,14 +584,14 @@ describe('emoji', () => {
'HEART',
[
{
- name: 'black_heart',
- field: 'd',
- score: 6,
- },
- {
name: 'heart',
field: 'name',
- score: 0,
+ score: NEUTRAL_INTENT_MULTIPLIER,
+ },
+ {
+ name: 'black_heart',
+ field: 'd',
+ score: 64,
},
],
],
@@ -585,14 +600,30 @@ describe('emoji', () => {
'star',
[
{
+ name: 'star',
+ field: 'name',
+ score: NEUTRAL_INTENT_MULTIPLIER,
+ },
+ {
name: 'custard',
field: 'd',
- score: 2,
+ score: 4,
+ },
+ ],
+ ],
+ [
+ 'searching for emoji with intentions assigned',
+ 'thumbs',
+ [
+ {
+ name: 'thumbsup',
+ field: 'd',
+ score: 0.5,
},
{
- name: 'star',
- field: 'name',
- score: 0,
+ name: 'thumbsdown',
+ field: 'd',
+ score: 1.5,
},
],
],
@@ -619,10 +650,10 @@ describe('emoji', () => {
[
{ score: 10, fieldValue: '', emoji: { name: 'a' } },
{ score: 5, fieldValue: '', emoji: { name: 'b' } },
- { score: 0, fieldValue: '', emoji: { name: 'c' } },
+ { score: 1, fieldValue: '', emoji: { name: 'c' } },
],
[
- { score: 0, fieldValue: '', emoji: { name: 'c' } },
+ { score: 1, fieldValue: '', emoji: { name: 'c' } },
{ score: 5, fieldValue: '', emoji: { name: 'b' } },
{ score: 10, fieldValue: '', emoji: { name: 'a' } },
],
@@ -630,25 +661,25 @@ describe('emoji', () => {
[
'should correctly sort by fieldValue',
[
- { score: 0, fieldValue: 'y', emoji: { name: 'b' } },
- { score: 0, fieldValue: 'x', emoji: { name: 'a' } },
- { score: 0, fieldValue: 'z', emoji: { name: 'c' } },
+ { score: 1, fieldValue: 'y', emoji: { name: 'b' } },
+ { score: 1, fieldValue: 'x', emoji: { name: 'a' } },
+ { score: 1, fieldValue: 'z', emoji: { name: 'c' } },
],
[
- { score: 0, fieldValue: 'x', emoji: { name: 'a' } },
- { score: 0, fieldValue: 'y', emoji: { name: 'b' } },
- { score: 0, fieldValue: 'z', emoji: { name: 'c' } },
+ { score: 1, fieldValue: 'x', emoji: { name: 'a' } },
+ { score: 1, fieldValue: 'y', emoji: { name: 'b' } },
+ { score: 1, fieldValue: 'z', emoji: { name: 'c' } },
],
],
[
'should correctly sort by score and then by fieldValue (in order)',
[
{ score: 5, fieldValue: 'y', emoji: { name: 'c' } },
- { score: 0, fieldValue: 'z', emoji: { name: 'a' } },
+ { score: 1, fieldValue: 'z', emoji: { name: 'a' } },
{ score: 5, fieldValue: 'x', emoji: { name: 'b' } },
],
[
- { score: 0, fieldValue: 'z', emoji: { name: 'a' } },
+ { score: 1, fieldValue: 'z', emoji: { name: 'a' } },
{ score: 5, fieldValue: 'x', emoji: { name: 'b' } },
{ score: 5, fieldValue: 'y', emoji: { name: 'c' } },
],
@@ -656,7 +687,7 @@ describe('emoji', () => {
];
it.each(testCases)('%s', (_, scoredItems, expected) => {
- expect(sortEmoji(scoredItems)).toEqual(expected);
+ expect(scoredItems.sort(sortEmoji)).toEqual(expected);
});
});
});
diff --git a/spec/frontend/emoji/utils_spec.js b/spec/frontend/emoji/utils_spec.js
new file mode 100644
index 00000000000..397388ca0ae
--- /dev/null
+++ b/spec/frontend/emoji/utils_spec.js
@@ -0,0 +1,15 @@
+import { getEmojiScoreWithIntent } from '~/emoji/utils';
+
+describe('Utils', () => {
+ describe('getEmojiScoreWithIntent', () => {
+ it.each`
+ emojiName | baseScore | finalScore
+ ${'thumbsup'} | ${1} | ${1}
+ ${'thumbsdown'} | ${1} | ${3}
+ ${'neutralemoji'} | ${1} | ${2}
+ ${'zerobaseemoji'} | ${0} | ${1}
+ `('returns the correct score for $emojiName', ({ emojiName, baseScore, finalScore }) => {
+ expect(getEmojiScoreWithIntent(emojiName, baseScore)).toBe(finalScore);
+ });
+ });
+});
diff --git a/spec/frontend/environments/deploy_board_wrapper_spec.js b/spec/frontend/environments/deploy_board_wrapper_spec.js
index c8e6df4d324..49eed68fa11 100644
--- a/spec/frontend/environments/deploy_board_wrapper_spec.js
+++ b/spec/frontend/environments/deploy_board_wrapper_spec.js
@@ -57,7 +57,7 @@ describe('~/environments/components/deploy_board_wrapper.vue', () => {
it('is collapsed by default', () => {
expect(collapse.attributes('visible')).toBeUndefined();
- expect(icon.props('name')).toBe('angle-right');
+ expect(icon.props('name')).toBe('chevron-lg-right');
});
it('opens on click', async () => {
@@ -65,7 +65,7 @@ describe('~/environments/components/deploy_board_wrapper.vue', () => {
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.attributes('visible')).toBe('visible');
- expect(icon.props('name')).toBe('angle-down');
+ expect(icon.props('name')).toBe('chevron-lg-down');
const deployBoard = findDeployBoard();
expect(deployBoard.exists()).toBe(true);
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index 37b897bf65d..48624f2324b 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -82,7 +82,7 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(collapse.attributes('visible')).toBeUndefined();
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
- expect(iconNames).toEqual(['angle-right', 'folder-o']);
+ expect(iconNames).toEqual(['chevron-lg-right', 'folder-o']);
expect(folderName.classes('gl-font-weight-bold')).toBe(false);
expect(link.exists()).toBe(false);
});
@@ -95,7 +95,7 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.attributes('visible')).toBe('visible');
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
- expect(iconNames).toEqual(['angle-down', 'folder-open']);
+ expect(iconNames).toEqual(['chevron-lg-down', 'folder-open']);
expect(folderName.classes('gl-font-weight-bold')).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index cf0c8a7e7ca..a151595bf64 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -374,7 +374,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('is collapsed by default', () => {
expect(collapse.attributes('visible')).toBeUndefined();
- expect(icon.props('name')).toEqual('angle-right');
+ expect(icon.props('name')).toBe('chevron-lg-right');
expect(environmentName.classes('gl-font-weight-bold')).toBe(false);
});
@@ -385,7 +385,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.attributes('visible')).toBe('visible');
- expect(icon.props('name')).toEqual('angle-down');
+ expect(icon.props('name')).toBe('chevron-lg-down');
expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
expect(findDeployment().isVisible()).toBe(true);
});
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index 4a0bbb1acbe..c660c9c4a99 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -177,7 +177,7 @@ describe('error tracking settings app', () => {
const clipBoardButton = findDsnSettings().findComponent(ClipboardButton);
expect(clipBoardInput.props('value')).toBe(TEST_GITLAB_DSN);
- expect(clipBoardInput.attributes('readonly')).toBeTruthy();
+ expect(clipBoardInput.attributes('readonly')).toBe('');
expect(clipBoardButton.props('text')).toBe(TEST_GITLAB_DSN);
});
});
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index 9c1657bc0d2..688ba54f919 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -61,7 +61,7 @@ describe('New feature flag form', () => {
});
it('renders form title', () => {
- expect(wrapper.find('h3').text()).toEqual('New feature flag');
+ expect(wrapper.text()).toContain('New feature flag');
});
it('should render feature flag form', () => {
diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/integrations.rb
index f0bb8fb962f..1bafb0bfe78 100644
--- a/spec/frontend/fixtures/services.rb
+++ b/spec/frontend/fixtures/integrations.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
+RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
+ 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 }
@@ -20,7 +20,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
remove_repository(project)
end
- it 'services/edit_service.html' do
+ it 'settings/integrations/edit.html' do
get :edit, params: {
namespace_id: namespace,
project_id: project,
diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_integration.rb
index aed73dc1096..883dbb929a2 100644
--- a/spec/frontend/fixtures/prometheus_service.rb
+++ b/spec/frontend/fixtures/prometheus_integration.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
+RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') }
let!(:integration) { create(:prometheus_integration, project: project) }
let(:user) { project.first_owner }
@@ -20,7 +20,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
remove_repository(project)
end
- it 'services/prometheus/prometheus_service.html' do
+ it 'integrations/prometheus/prometheus_integration.html' do
get :edit, params: {
namespace_id: namespace,
project_id: project,
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index e17e73a93c4..a79982fa647 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -26,6 +26,12 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project)
end
+ before do
+ allow(Gitlab::Ci::RunnerUpgradeCheck.instance)
+ .to receive(:check_runner_upgrade_status)
+ .and_return(:not_available)
+ end
+
describe do
before do
sign_in(admin)
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index 8220ea16342..eef5dc86c1a 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -117,7 +117,7 @@ describe('FrequentItemsListItemComponent', () => {
link.vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
- label: 'projects_dropdown_frequent_items_list_item',
+ label: 'projects_dropdown_frequent_items_list_item_git_lab_community_edition',
});
});
});
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
index 6412fe8bb33..50811f43fc3 100644
--- a/spec/frontend/google_tag_manager/index_spec.js
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -13,6 +13,7 @@ import {
trackCheckout,
trackTransaction,
trackAddToCartUsageTab,
+ getNamespaceId,
} from '~/google_tag_manager';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { logError } from '~/lib/logger';
@@ -401,6 +402,7 @@ describe('~/google_tag_manager/index', () => {
{
brand: 'GitLab',
category: 'DevOps',
+ dimension36: 'not available',
id,
name,
price: revenue.toString(),
@@ -478,4 +480,26 @@ describe('~/google_tag_manager/index', () => {
resetHTMLFixture();
});
});
+
+ describe('when getting the namespace_id from Snowplow standard context', () => {
+ describe('when window.gl.snowplowStandardContext.data.namespace_id has a value', () => {
+ beforeEach(() => {
+ window.gl = { snowplowStandardContext: { data: { namespace_id: '321' } } };
+ });
+
+ it('returns the value', () => {
+ expect(getNamespaceId()).toBe('321');
+ });
+ });
+
+ describe('when window.gl.snowplowStandardContext.data.namespace_id is undefined', () => {
+ beforeEach(() => {
+ window.gl = {};
+ });
+
+ it('returns a placeholder value', () => {
+ expect(getNamespaceId()).toBe('not available');
+ });
+ });
+ });
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 848e50c86ba..9e4666ffc70 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -10,8 +10,10 @@ import groupItemComponent from '~/groups/components/group_item.vue';
import eventHub from '~/groups/event_hub';
import GroupsService from '~/groups/service/groups_service';
import GroupsStore from '~/groups/store/groups_store';
+import EmptyState from '~/groups/components/empty_state.vue';
import axios from '~/lib/utils/axios_utils';
import * as urlUtilities from '~/lib/utils/url_utility';
+import setWindowLocation from 'helpers/set_window_location_helper';
import {
mockEndpoint,
@@ -38,17 +40,23 @@ describe('AppComponent', () => {
const store = new GroupsStore({ hideProjects: false });
const service = new GroupsService(mockEndpoint);
- const createShallowComponent = (hideProjects = false) => {
+ const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => {
store.state.pageInfo = mockPageInfo;
wrapper = shallowMount(appComponent, {
propsData: {
store,
service,
- hideProjects,
+ hideProjects: false,
+ containerId: 'js-groups-tree',
+ ...propsData,
},
mocks: {
$toast,
},
+ provide: {
+ renderEmptyState: false,
+ ...provide,
+ },
});
vm = wrapper.vm;
};
@@ -64,6 +72,14 @@ describe('AppComponent', () => {
Vue.component('GroupFolder', groupFolderComponent);
Vue.component('GroupItem', groupItemComponent);
+ document.body.innerHTML = `
+ <div id="js-groups-tree">
+ <div class="empty-state hidden" data-testid="legacy-empty-state">
+ <p>There are no projects shared with this group yet</p>
+ </div>
+ </div>
+ `;
+
createShallowComponent();
getGroupsSpy = jest.spyOn(vm.service, 'getGroups');
await nextTick();
@@ -386,7 +402,10 @@ describe('AppComponent', () => {
expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups);
});
- it('should set `isSearchEmpty` prop based on groups count', () => {
+ it('should set `isSearchEmpty` prop based on groups count and `filter` query param', () => {
+ setWindowLocation('?filter=foobar');
+ createShallowComponent();
+
vm.updateGroups(mockGroups);
expect(vm.isSearchEmpty).toBe(false);
@@ -395,6 +414,47 @@ describe('AppComponent', () => {
expect(vm.isSearchEmpty).toBe(true);
});
+
+ describe.each`
+ action | groups | fromSearch | renderEmptyState | expected
+ ${'subgroups_and_projects'} | ${[]} | ${false} | ${true} | ${true}
+ ${''} | ${[]} | ${false} | ${true} | ${false}
+ ${'subgroups_and_projects'} | ${mockGroups} | ${false} | ${true} | ${false}
+ ${'subgroups_and_projects'} | ${[]} | ${true} | ${true} | ${false}
+ `(
+ 'when `action` is $action, `groups` is $groups, `fromSearch` is $fromSearch, and `renderEmptyState` is $renderEmptyState',
+ ({ action, groups, fromSearch, renderEmptyState, expected }) => {
+ it(expected ? 'renders empty state' : 'does not render empty state', async () => {
+ createShallowComponent({
+ propsData: { action },
+ provide: { renderEmptyState },
+ });
+
+ vm.updateGroups(groups, fromSearch);
+
+ await nextTick();
+
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(expected);
+ });
+ },
+ );
+ });
+
+ describe('when `action` is subgroups_and_projects, `groups` is [], `fromSearch` is `false`, and `renderEmptyState` is `false`', () => {
+ it('renders legacy empty state', async () => {
+ createShallowComponent({
+ propsData: { action: 'subgroups_and_projects' },
+ provide: { renderEmptyState: false },
+ });
+
+ vm.updateGroups([], false);
+
+ await nextTick();
+
+ expect(
+ document.querySelector('[data-testid="legacy-empty-state"]').classList.contains('hidden'),
+ ).toBe(false);
+ });
});
});
@@ -419,7 +479,7 @@ describe('AppComponent', () => {
});
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', async () => {
- createShallowComponent(true);
+ createShallowComponent({ propsData: { hideProjects: true } });
await nextTick();
expect(vm.searchEmptyMessage).toBe('No groups matched your search');
});
diff --git a/spec/frontend/groups/components/empty_state_spec.js b/spec/frontend/groups/components/empty_state_spec.js
new file mode 100644
index 00000000000..c0e71e814d0
--- /dev/null
+++ b/spec/frontend/groups/components/empty_state_spec.js
@@ -0,0 +1,78 @@
+import { GlEmptyState } from '@gitlab/ui';
+
+import { mountExtended } from 'jest/__helpers__/vue_test_utils_helper';
+import EmptyState from '~/groups/components/empty_state.vue';
+
+let wrapper;
+
+const defaultProvide = {
+ newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+ newProjectPath: '/projects/new?namespace_id=231',
+ newSubgroupIllustration: '/assets/illustrations/group-new.svg',
+ newSubgroupPath: '/groups/new?parent_id=231',
+ emptySubgroupIllustration: '/assets/illustrations/empty-state/empty-subgroup-md.svg',
+ canCreateSubgroups: true,
+ canCreateProjects: true,
+};
+
+const createComponent = ({ provide = {} } = {}) => {
+ wrapper = mountExtended(EmptyState, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ });
+};
+
+afterEach(() => {
+ wrapper.destroy();
+});
+
+const findNewSubgroupLink = () =>
+ wrapper.findByRole('link', {
+ name: new RegExp(EmptyState.i18n.withLinks.subgroup.title),
+ });
+const findNewProjectLink = () =>
+ wrapper.findByRole('link', {
+ name: new RegExp(EmptyState.i18n.withLinks.project.title),
+ });
+const findNewSubgroupIllustration = () =>
+ wrapper.findByRole('img', { name: EmptyState.i18n.withLinks.subgroup.title });
+const findNewProjectIllustration = () =>
+ wrapper.findByRole('img', { name: EmptyState.i18n.withLinks.project.title });
+
+describe('EmptyState', () => {
+ describe('when user has permission to create a subgroup', () => {
+ it('renders `Create new subgroup` link', () => {
+ createComponent();
+
+ expect(findNewSubgroupLink().attributes('href')).toBe(defaultProvide.newSubgroupPath);
+ expect(findNewSubgroupIllustration().attributes('src')).toBe(
+ defaultProvide.newSubgroupIllustration,
+ );
+ });
+ });
+
+ describe('when user has permission to create a project', () => {
+ it('renders `Create new project` link', () => {
+ createComponent();
+
+ expect(findNewProjectLink().attributes('href')).toBe(defaultProvide.newProjectPath);
+ expect(findNewProjectIllustration().attributes('src')).toBe(
+ defaultProvide.newProjectIllustration,
+ );
+ });
+ });
+
+ describe('when user does not have permissions to create a project or a subgroup', () => {
+ it('renders empty state', () => {
+ createComponent({ provide: { canCreateSubgroups: false, canCreateProjects: false } });
+
+ expect(wrapper.find(GlEmptyState).props()).toMatchObject({
+ title: EmptyState.i18n.withoutLinks.title,
+ description: EmptyState.i18n.withoutLinks.description,
+ svgPath: defaultProvide.emptySubgroupIllustration,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js
new file mode 100644
index 00000000000..eaa0801ab50
--- /dev/null
+++ b/spec/frontend/groups/components/group_name_and_path_spec.js
@@ -0,0 +1,347 @@
+import { merge } from 'lodash';
+import { GlAlert } from '@gitlab/ui';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import GroupNameAndPath from '~/groups/components/group_name_and_path.vue';
+import { getGroupPathAvailability } from '~/rest_api';
+import { createAlert } from '~/flash';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+jest.mock('~/flash');
+jest.mock('~/rest_api', () => ({
+ getGroupPathAvailability: jest.fn(),
+}));
+
+describe('GroupNameAndPath', () => {
+ let wrapper;
+
+ const mockGroupName = 'My awesome group';
+ const mockGroupUrl = 'my-awesome-group';
+ const mockGroupUrlSuggested = 'my-awesome-group1';
+
+ const defaultProvide = {
+ basePath: 'http://gitlab.com/',
+ fields: {
+ name: { name: 'group[name]', id: 'group_name', value: '' },
+ path: {
+ name: 'group[path]',
+ id: 'group_path',
+ value: '',
+ maxLength: 255,
+ pattern: '[a-zA-Z0-9_\\.][a-zA-Z0-9_\\-\\.]*[a-zA-Z0-9_\\-]|[a-zA-Z0-9_]',
+ },
+ parentId: { name: 'group[parent_id]', id: 'group_parent_id', value: '1' },
+ groupId: { name: 'group[id]', id: 'group_id', value: '' },
+ },
+ mattermostEnabled: false,
+ };
+
+ const createComponent = ({ provide = {} } = {}) => {
+ wrapper = mountExtended(GroupNameAndPath, { provide: merge({}, defaultProvide, provide) });
+ };
+ const createComponentEditGroup = ({ path = mockGroupUrl } = {}) => {
+ createComponent({
+ provide: { fields: { groupId: { value: '1' }, path: { value: path } } },
+ });
+ };
+
+ const findGroupNameField = () => wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.name.label);
+ const findGroupUrlField = () => wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.path.label);
+ const findAlert = () => extendedWrapper(wrapper.findComponent(GlAlert));
+
+ const apiMockAvailablePath = () => {
+ getGroupPathAvailability.mockResolvedValue({
+ data: { exists: false, suggests: [] },
+ });
+ };
+ const apiMockUnavailablePath = (suggests = [mockGroupUrlSuggested]) => {
+ getGroupPathAvailability.mockResolvedValue({
+ data: { exists: true, suggests },
+ });
+ };
+ const apiMockLoading = () => {
+ getGroupPathAvailability.mockImplementation(() => new Promise(() => {}));
+ };
+
+ const expectLoadingMessageExists = () => {
+ expect(wrapper.findByText(GroupNameAndPath.i18n.apiLoadingMessage).exists()).toBe(true);
+ };
+
+ describe('when user types in the `Group name` field', () => {
+ describe('when creating a new group', () => {
+ it('updates `Group URL` field as user types', async () => {
+ createComponent();
+
+ await findGroupNameField().setValue(mockGroupName);
+
+ expect(findGroupUrlField().element.value).toBe(mockGroupUrl);
+ });
+ });
+
+ describe('when editing a group', () => {
+ it('does not update `Group URL` field and does not call API', async () => {
+ const groupUrl = 'foo-bar';
+
+ createComponentEditGroup({ path: groupUrl });
+
+ await findGroupNameField().setValue(mockGroupName);
+
+ expect(findGroupUrlField().element.value).toBe(groupUrl);
+ expect(getGroupPathAvailability).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `Group URL` field has been manually entered', () => {
+ it('does not update `Group URL` field and does not call API', async () => {
+ apiMockAvailablePath();
+
+ createComponent();
+
+ await findGroupUrlField().setValue(mockGroupUrl);
+ await waitForPromises();
+
+ getGroupPathAvailability.mockClear();
+
+ await findGroupNameField().setValue('Foo bar');
+
+ expect(findGroupUrlField().element.value).toBe(mockGroupUrl);
+ expect(getGroupPathAvailability).not.toHaveBeenCalled();
+ });
+ });
+
+ it('shows loading message', async () => {
+ apiMockLoading();
+
+ createComponent();
+
+ await findGroupNameField().setValue(mockGroupName);
+
+ expectLoadingMessageExists();
+ });
+
+ describe('when path is available', () => {
+ it('does not update `Group URL` field', async () => {
+ apiMockAvailablePath();
+
+ createComponent();
+
+ await findGroupNameField().setValue(mockGroupName);
+
+ expect(getGroupPathAvailability).toHaveBeenCalledWith(
+ mockGroupUrl,
+ defaultProvide.fields.parentId.value,
+ { signal: expect.any(AbortSignal) },
+ );
+
+ await waitForPromises();
+
+ expect(findGroupUrlField().element.value).toBe(mockGroupUrl);
+ });
+ });
+
+ describe('when path is not available', () => {
+ it('updates `Group URL` field', async () => {
+ apiMockUnavailablePath();
+
+ createComponent();
+
+ await findGroupNameField().setValue(mockGroupName);
+ await waitForPromises();
+
+ expect(findGroupUrlField().element.value).toBe(mockGroupUrlSuggested);
+ });
+ });
+
+ describe('when API returns no suggestions', () => {
+ it('calls `createAlert`', async () => {
+ apiMockUnavailablePath([]);
+
+ createComponent();
+
+ await findGroupNameField().setValue(mockGroupName);
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: GroupNameAndPath.i18n.apiErrorMessage,
+ });
+ });
+ });
+
+ describe('when API call fails', () => {
+ it('calls `createAlert`', async () => {
+ getGroupPathAvailability.mockRejectedValue({});
+
+ createComponent();
+
+ await findGroupNameField().setValue(mockGroupName);
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: GroupNameAndPath.i18n.apiErrorMessage,
+ });
+ });
+ });
+
+ describe('when multiple API calls are in-flight', () => {
+ it('aborts the first API call and resolves second API call', async () => {
+ apiMockLoading();
+ apiMockUnavailablePath();
+ const abortSpy = jest.spyOn(AbortController.prototype, 'abort');
+
+ createComponent();
+
+ await findGroupNameField().setValue('Foo');
+ await findGroupNameField().setValue(mockGroupName);
+ await waitForPromises();
+
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(findGroupUrlField().element.value).toBe(mockGroupUrlSuggested);
+ expect(abortSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('when `Group URL` is empty', () => {
+ it('does not call API', async () => {
+ createComponent({
+ provide: { fields: { name: { value: mockGroupName }, path: mockGroupUrl } },
+ });
+
+ await findGroupNameField().setValue('');
+
+ expect(getGroupPathAvailability).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when `Group name` field is invalid', () => {
+ it('shows error message', async () => {
+ createComponent();
+
+ await findGroupNameField().trigger('invalid');
+
+ expect(wrapper.findByText(GroupNameAndPath.i18n.inputs.name.invalidFeedback).exists()).toBe(
+ true,
+ );
+ });
+ });
+
+ describe('when user types in `Group URL` field', () => {
+ it('shows loading message', async () => {
+ apiMockLoading();
+
+ createComponent();
+
+ await findGroupUrlField().setValue(mockGroupUrl);
+
+ expectLoadingMessageExists();
+ });
+
+ describe('when path is available', () => {
+ it('displays success message', async () => {
+ apiMockAvailablePath();
+
+ createComponent();
+
+ await findGroupUrlField().setValue(mockGroupUrl);
+ await waitForPromises();
+
+ expect(wrapper.findByText(GroupNameAndPath.i18n.inputs.path.validFeedback).exists()).toBe(
+ true,
+ );
+ });
+ });
+
+ describe('when path is not available', () => {
+ it('displays error message and updates `Group URL` field', async () => {
+ apiMockUnavailablePath();
+
+ createComponent();
+
+ await findGroupUrlField().setValue(mockGroupUrl);
+ await waitForPromises();
+
+ expect(
+ wrapper
+ .findByText(GroupNameAndPath.i18n.inputs.path.invalidFeedbackPathUnavailable)
+ .exists(),
+ ).toBe(true);
+ expect(findGroupUrlField().element.value).toBe(mockGroupUrlSuggested);
+ });
+ });
+
+ describe('when editing a group', () => {
+ it('calls API if `Group URL` does not equal the original `Group URL`', async () => {
+ const groupUrl = 'foo-bar';
+
+ apiMockAvailablePath();
+
+ createComponentEditGroup({ path: groupUrl });
+
+ await findGroupUrlField().setValue('foo-bar1');
+ await waitForPromises();
+
+ expect(getGroupPathAvailability).toHaveBeenCalled();
+ expect(wrapper.findByText(GroupNameAndPath.i18n.inputs.path.validFeedback).exists()).toBe(
+ true,
+ );
+
+ getGroupPathAvailability.mockClear();
+
+ await findGroupUrlField().setValue('foo-bar');
+
+ expect(getGroupPathAvailability).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when `Group URL` field is invalid', () => {
+ it('shows error message', async () => {
+ createComponent();
+
+ await findGroupUrlField().trigger('invalid');
+
+ expect(
+ wrapper
+ .findByText(GroupNameAndPath.i18n.inputs.path.invalidFeedbackInvalidPattern)
+ .exists(),
+ ).toBe(true);
+ });
+ });
+
+ describe('mattermost', () => {
+ it('adds `data-bind-in` attribute when enabled', () => {
+ createComponent({ provide: { mattermostEnabled: true } });
+
+ expect(findGroupUrlField().attributes('data-bind-in')).toBe(
+ GroupNameAndPath.mattermostDataBindName,
+ );
+ });
+
+ it('does not add `data-bind-in` attribute when disabled', () => {
+ createComponent();
+
+ expect(findGroupUrlField().attributes('data-bind-in')).toBeUndefined();
+ });
+ });
+
+ describe('when editing a group', () => {
+ it('shows warning alert with `Learn more` link', () => {
+ createComponentEditGroup();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().findByRole('link', { name: 'Learn more' }).attributes('href')).toBe(
+ helpPagePath('user/group/index', {
+ anchor: 'change-a-groups-path',
+ }),
+ );
+ });
+
+ it('shows `Group ID` field', () => {
+ createComponentEditGroup();
+
+ expect(
+ wrapper.findByLabelText(GroupNameAndPath.i18n.inputs.groupId.label).element.value,
+ ).toBe('1');
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js
index cbe1f21d6e2..4bf92bb5642 100644
--- a/spec/frontend/groups/components/item_caret_spec.js
+++ b/spec/frontend/groups/components/item_caret_spec.js
@@ -35,8 +35,8 @@ describe('ItemCaret', () => {
it.each`
isGroupOpen | icon
- ${true} | ${'angle-down'}
- ${false} | ${'angle-right'}
+ ${true} | ${'chevron-down'}
+ ${false} | ${'chevron-right'}
`('renders "$icon" icon when `isGroupOpen` is $isGroupOpen', ({ isGroupOpen, icon }) => {
createComponent({
isGroupOpen,
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
index 2236b5aa261..05161437c22 100644
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -59,9 +59,10 @@ describe('waitForCSSLoaded', () => {
<link href="two.css" data-startupcss="loading">
`);
const events = waitForCSSLoaded(mockedCallback);
- document
- .querySelectorAll('[data-startupcss="loading"]')
- .forEach((elem) => elem.setAttribute('data-startupcss', 'loaded'));
+ document.querySelectorAll('[data-startupcss="loading"]').forEach((elem) => {
+ // eslint-disable-next-line no-param-reassign
+ elem.dataset.startupcss = 'loaded';
+ });
document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
await events;
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index 6e4c66cb780..d77e8e3d04c 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -68,7 +68,7 @@ describe('IDE commit editor header', () => {
it('calls discardFileChanges if dialog result is confirmed', () => {
expect(store.dispatch).not.toHaveBeenCalled();
- findDiscardModal().vm.$emit('ok');
+ findDiscardModal().vm.$emit('primary');
expect(store.dispatch).toHaveBeenCalledWith('discardFileChanges', TEST_FILE_PATH);
});
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 64b53264b4d..2a455c9d7c1 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -1,193 +1,97 @@
-import Vue, { nextTick } from 'vue';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { projectData, branches } from 'jest/ide/mock_data';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { GlFormCheckbox } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
-import { PERMISSION_CREATE_MR } from '~/ide/constants';
import { createStore } from '~/ide/stores';
-import {
- COMMIT_TO_CURRENT_BRANCH,
- COMMIT_TO_NEW_BRANCH,
-} from '~/ide/stores/modules/commit/constants';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-describe('create new MR checkbox', () => {
- let store;
- let vm;
-
- const setMR = () => {
- vm.$store.state.currentMergeRequestId = '1';
- vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
- store.state.currentMergeRequestId
- ] = { foo: 'bar' };
- };
-
- const setPermissions = (permissions) => {
- store.state.projects[store.state.currentProjectId].userPermissions = permissions;
- };
-
- const createComponent = ({ currentBranchId = 'main', createNewBranch = false } = {}) => {
- const Component = Vue.extend(NewMergeRequestOption);
-
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.commit.commitAction = createNewBranch
- ? COMMIT_TO_NEW_BRANCH
- : COMMIT_TO_CURRENT_BRANCH;
+Vue.use(Vuex);
- vm.$store.state.currentBranchId = currentBranchId;
-
- store.state.projects.abcproject.branches[currentBranchId] = branches.find(
- (branch) => branch.name === currentBranchId,
- );
-
- return vm.$mount();
- };
+describe('NewMergeRequestOption component', () => {
+ let store;
+ let wrapper;
- const findInput = () => vm.$el.querySelector('input[type="checkbox"]');
- const findLabel = () => vm.$el.querySelector('.js-ide-commit-new-mr');
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findFieldset = () => wrapper.findByTestId('new-merge-request-fieldset');
+ const findTooltip = () => getBinding(findFieldset().element, 'gl-tooltip');
- beforeEach(() => {
+ const createComponent = ({
+ shouldHideNewMrOption = false,
+ shouldDisableNewMrOption = false,
+ shouldCreateMR = false,
+ } = {}) => {
store = createStore();
- store.state.currentProjectId = 'abcproject';
-
- const proj = JSON.parse(JSON.stringify(projectData));
- proj.userPermissions[PERMISSION_CREATE_MR] = true;
- Vue.set(store.state.projects, 'abcproject', proj);
- });
+ wrapper = shallowMountExtended(NewMergeRequestOption, {
+ store: {
+ ...store,
+ getters: {
+ 'commit/shouldHideNewMrOption': shouldHideNewMrOption,
+ 'commit/shouldDisableNewMrOption': shouldDisableNewMrOption,
+ 'commit/shouldCreateMR': shouldCreateMR,
+ },
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('for default branch', () => {
- describe('is rendered when pushing to a new branch', () => {
- beforeEach(() => {
- createComponent({
- currentBranchId: 'main',
- createNewBranch: true,
- });
- });
-
- it('has NO new MR', () => {
- expect(vm.$el.textContent).not.toBe('');
- });
-
- it('has new MR', async () => {
- setMR();
-
- await nextTick();
- expect(vm.$el.textContent).not.toBe('');
- });
+ describe('when the `shouldHideNewMrOption` getter returns false', () => {
+ beforeEach(() => {
+ createComponent();
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
- describe('is NOT rendered when pushing to the same branch', () => {
- beforeEach(() => {
- createComponent({
- currentBranchId: 'main',
- createNewBranch: false,
- });
- });
-
- it('has NO new MR', () => {
- expect(vm.$el.textContent).toBe('');
- });
-
- it('has new MR', async () => {
- setMR();
-
- await nextTick();
- expect(vm.$el.textContent).toBe('');
- });
+ it('renders an enabled new MR checkbox', () => {
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
});
- });
- describe('for protected branch', () => {
- describe('when user does not have the write access', () => {
- beforeEach(() => {
- createComponent({
- currentBranchId: 'protected/no-access',
- });
- });
-
- it('is rendered if MR does not exists', () => {
- expect(vm.$el.textContent).not.toBe('');
- });
+ it("doesn't add `is-disabled` class to the fieldset", () => {
+ expect(findFieldset().classes()).not.toContain('is-disabled');
+ });
- it('is rendered if MR exists', async () => {
- setMR();
+ it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
+ findCheckbox().vm.$emit('change');
- await nextTick();
- expect(vm.$el.textContent).not.toBe('');
- });
+ expect(store.dispatch).toHaveBeenCalledWith('commit/toggleShouldCreateMR', undefined);
});
- describe('when user has the write access', () => {
+ describe('when user cannot create an MR', () => {
beforeEach(() => {
createComponent({
- currentBranchId: 'protected/access',
+ shouldDisableNewMrOption: true,
});
});
- it('is rendered if MR does not exist', () => {
- expect(vm.$el.textContent).not.toBe('');
+ it('disables the new MR checkbox', () => {
+ expect(findCheckbox().attributes('disabled')).toBe('true');
});
- it('is hidden if MR exists', async () => {
- setMR();
+ it('adds `is-disabled` class to the fieldset', () => {
+ expect(findFieldset().classes()).toContain('is-disabled');
+ });
- await nextTick();
- expect(vm.$el.textContent).toBe('');
+ it('shows a tooltip', () => {
+ expect(findTooltip().value).toBe(wrapper.vm.$options.i18n.tooltipText);
});
});
});
- describe('for regular branch', () => {
+ describe('when the `shouldHideNewMrOption` getter returns true', () => {
beforeEach(() => {
createComponent({
- currentBranchId: 'regular',
+ shouldHideNewMrOption: true,
});
});
- it('is rendered if no MR exists', () => {
- expect(vm.$el.textContent).not.toBe('');
- });
-
- it('is hidden if MR exists', async () => {
- setMR();
-
- await nextTick();
- expect(vm.$el.textContent).toBe('');
- });
-
- it('shows enablded checkbox', () => {
- expect(findLabel().classList.contains('is-disabled')).toBe(false);
- expect(findInput().disabled).toBe(false);
+ it("doesn't render the new MR checkbox", () => {
+ expect(findCheckbox().exists()).toBe(false);
});
});
-
- describe('when user cannot create MR', () => {
- beforeEach(() => {
- setPermissions({ [PERMISSION_CREATE_MR]: false });
-
- createComponent({ currentBranchId: 'regular' });
- });
-
- it('disabled checkbox', () => {
- expect(findLabel().classList.contains('is-disabled')).toBe(true);
- expect(findInput().disabled).toBe(true);
- });
- });
-
- it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
- createComponent({
- currentBranchId: 'regular',
- });
- const el = vm.$el.querySelector('input[type="checkbox"]');
- jest.spyOn(vm.$store, 'dispatch').mockImplementation(() => {});
- el.dispatchEvent(new Event('change'));
-
- expect(vm.$store.dispatch.mock.calls).toEqual(
- expect.arrayContaining([['commit/toggleShouldCreateMR', expect.any(Object)]]),
- );
- });
});
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index ace8988b8c9..4469c3fc901 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -47,7 +47,7 @@ describe('IdeSidebar', () => {
await nextTick();
- expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(3);
+ expect(wrapper.findAll(GlSkeletonLoader)).toHaveLength(3);
});
describe('deferred rendering components', () => {
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index 00ef75fcf3a..17a5aa17b1f 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -11,6 +11,8 @@ const TEST_PROJECT_ID = 'abcproject';
const TEST_MERGE_REQUEST_ID = '9001';
const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_ID}`;
+jest.mock('~/lib/utils/poll');
+
describe('ideStatusBar', () => {
let store;
let vm;
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
index d1cf9f2e248..45444166a50 100644
--- a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -35,7 +35,7 @@ exports[`IDE pipeline stage renders stage details & icon 1`] = `
<gl-icon-stub
class="ide-stage-collapse-icon"
- name="angle-down"
+ name="chevron-lg-down"
size="16"
/>
</div>
diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index 786a7661d97..128ccff6568 100644
--- a/spec/frontend/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
@@ -28,6 +28,12 @@ describe('IDE job description', () => {
).not.toBe(null);
});
+ it('renders a borderless CI icon', () => {
+ expect(
+ vm.$el.querySelector('.borderless [data-testid="status_success_borderless-icon"]'),
+ ).not.toBe(null);
+ });
+
it('renders bridge job details without the job link', () => {
vm = mountComponent(Component, {
job: { ...jobs[0], path: undefined },
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 9a30fd5f5c3..b44651481e9 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { editor as monacoEditor, Range } from 'monaco-editor';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { shallowMount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
import waitForPromises from 'helpers/wait_for_promises';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
@@ -11,57 +11,54 @@ import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markd
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import SourceEditor from '~/editor/source_editor';
import RepoEditor from '~/ide/components/repo_editor.vue';
-import {
- leftSidebarViews,
- FILE_VIEW_MODE_EDITOR,
- FILE_VIEW_MODE_PREVIEW,
- viewerTypes,
-} from '~/ide/constants';
+import { leftSidebarViews, FILE_VIEW_MODE_PREVIEW, viewerTypes } from '~/ide/constants';
import ModelManager from '~/ide/lib/common/model_manager';
import service from '~/ide/services';
import { createStoreOptions } from '~/ide/stores';
import axios from '~/lib/utils/axios_utils';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import SourceEditorInstance from '~/editor/source_editor_instance';
-import { spyOnApi } from 'jest/editor/helpers';
import { file } from '../helpers';
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const CURRENT_PROJECT_ID = 'gitlab-org/gitlab';
-const defaultFileProps = {
- ...file('file.txt'),
- content: 'hello world',
- active: true,
- tempFile: true,
+const dummyFile = {
+ text: {
+ ...file('file.txt'),
+ content: 'hello world',
+ active: true,
+ tempFile: true,
+ },
+ markdown: {
+ ...file('sample.md'),
+ projectId: 'namespace/project',
+ path: 'sample.md',
+ content: 'hello world',
+ tempFile: true,
+ active: true,
+ },
+ binary: {
+ ...file('file.dat'),
+ content: '🐱', // non-ascii binary content,
+ tempFile: true,
+ active: true,
+ },
+ empty: {
+ ...file('empty'),
+ tempFile: false,
+ content: '',
+ raw: '',
+ },
};
+
const createActiveFile = (props) => {
return {
- ...defaultFileProps,
+ ...dummyFile.text,
...props,
};
};
-const dummyFile = {
- markdown: (() =>
- createActiveFile({
- projectId: 'namespace/project',
- path: 'sample.md',
- name: 'sample.md',
- }))(),
- binary: (() =>
- createActiveFile({
- name: 'file.dat',
- content: '🐱', // non-ascii binary content,
- }))(),
- empty: (() =>
- createActiveFile({
- tempFile: false,
- content: '',
- raw: '',
- }))(),
-};
-
const prepareStore = (state, activeFile) => {
const localState = {
openFiles: [activeFile],
@@ -109,7 +106,7 @@ describe('RepoEditor', () => {
vm.$once('editorSetup', resolve);
});
- const createComponent = async ({ state = {}, activeFile = defaultFileProps } = {}) => {
+ const createComponent = async ({ state = {}, activeFile = dummyFile.text } = {}) => {
const store = prepareStore(state, activeFile);
wrapper = shallowMount(RepoEditor, {
store,
@@ -187,7 +184,7 @@ describe('RepoEditor', () => {
mock = new MockAdapter(axios);
mock.onPost(/(.*)\/preview_markdown/).reply(200, {
- body: `<p>${defaultFileProps.content}</p>`,
+ body: `<p>${dummyFile.text.content}</p>`,
});
});
@@ -196,11 +193,8 @@ describe('RepoEditor', () => {
});
describe('when files is markdown', () => {
- let layoutSpy;
-
beforeEach(async () => {
await createComponent({ activeFile });
- layoutSpy = jest.spyOn(wrapper.vm.editor, 'layout');
});
it('renders an Edit and a Preview Tab', () => {
@@ -214,11 +208,7 @@ describe('RepoEditor', () => {
it('renders markdown for tempFile', async () => {
findPreviewTab().trigger('click');
await waitForPromises();
- expect(wrapper.find(ContentViewer).html()).toContain(defaultFileProps.content);
- });
-
- it('should not trigger layout', async () => {
- expect(layoutSpy).not.toHaveBeenCalled();
+ expect(wrapper.find(ContentViewer).html()).toContain(dummyFile.text.content);
});
describe('when file changes to non-markdown file', () => {
@@ -229,10 +219,6 @@ describe('RepoEditor', () => {
it('should hide tabs', () => {
expect(findTabs()).toHaveLength(0);
});
-
- it('should trigger refresh dimensions', async () => {
- expect(layoutSpy).toHaveBeenCalledTimes(1);
- });
});
});
@@ -292,55 +278,20 @@ describe('RepoEditor', () => {
expect(vm.editor.methods[fn]).toBe('EditorWebIde');
});
});
-
- it.each`
- prefix | activeFile | viewer | shouldHaveMarkdownExtension
- ${'Should not'} | ${createActiveFile()} | ${viewerTypes.edit} | ${false}
- ${'Should'} | ${dummyFile.markdown} | ${viewerTypes.edit} | ${true}
- ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.edit} | ${false}
- ${'Should not'} | ${createActiveFile()} | ${viewerTypes.diff} | ${false}
- ${'Should not'} | ${dummyFile.markdown} | ${viewerTypes.diff} | ${false}
- ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.diff} | ${false}
- ${'Should not'} | ${createActiveFile()} | ${viewerTypes.mr} | ${false}
- ${'Should not'} | ${dummyFile.markdown} | ${viewerTypes.mr} | ${false}
- ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.mr} | ${false}
- `(
- '$prefix install markdown extension for $activeFile.name in $viewer viewer',
- async ({ activeFile, viewer, shouldHaveMarkdownExtension } = {}) => {
- await createComponent({ state: { viewer }, activeFile });
-
- if (shouldHaveMarkdownExtension) {
- expect(applyExtensionSpy).toHaveBeenCalledWith({
- definition: EditorMarkdownPreviewExtension,
- setupOptions: { previewMarkdownPath: PREVIEW_MARKDOWN_PATH },
- });
- // TODO: spying on extensions causes Jest to blow up, so we have to assert on
- // the public property the extension adds, as opposed to the args passed to the ctor
- expect(wrapper.vm.editor.markdownPreview.path).toBe(PREVIEW_MARKDOWN_PATH);
- } else {
- expect(applyExtensionSpy).not.toHaveBeenCalledWith(
- wrapper.vm.editor,
- expect.any(EditorMarkdownExtension),
- );
- }
- },
- );
});
describe('setupEditor', () => {
- beforeEach(async () => {
+ it('creates new model on load', async () => {
await createComponent();
- });
-
- it('creates new model on load', () => {
// We always create two models per file to be able to build a diff of changes
expect(createModelSpy).toHaveBeenCalledTimes(2);
// The model with the most recent changes is the last one
const [content] = createModelSpy.mock.calls[1];
- expect(content).toBe(defaultFileProps.content);
+ expect(content).toBe(dummyFile.text.content);
});
- it('does not create a new model on subsequent calls to setupEditor and re-uses the already-existing model', () => {
+ it('does not create a new model on subsequent calls to setupEditor and re-uses the already-existing model', async () => {
+ await createComponent();
const existingModel = vm.model;
createModelSpy.mockClear();
@@ -350,7 +301,8 @@ describe('RepoEditor', () => {
expect(vm.model).toBe(existingModel);
});
- it('updates state with the value of the model', () => {
+ it('updates state with the value of the model', async () => {
+ await createComponent();
const newContent = 'As Gregor Samsa\n awoke one morning\n';
vm.model.setValue(newContent);
@@ -359,7 +311,8 @@ describe('RepoEditor', () => {
expect(vm.file.content).toBe(newContent);
});
- it('sets head model as staged file', () => {
+ it('sets head model as staged file', async () => {
+ await createComponent();
vm.modelManager.dispose();
const addModelSpy = jest.spyOn(ModelManager.prototype, 'addModel');
@@ -371,52 +324,54 @@ describe('RepoEditor', () => {
expect(addModelSpy).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]);
});
- });
-
- describe('editor updateDimensions', () => {
- let updateDimensionsSpy;
- beforeEach(async () => {
- await createComponent();
- const ext = extensionsStore.get('EditorWebIde');
- updateDimensionsSpy = jest.fn();
- spyOnApi(ext, {
- updateDimensions: updateDimensionsSpy,
- });
- });
- it('calls updateDimensions only when panelResizing is false', async () => {
- expect(updateDimensionsSpy).not.toHaveBeenCalled();
- expect(vm.$store.state.panelResizing).toBe(false); // default value
-
- vm.$store.state.panelResizing = true;
- await nextTick();
-
- expect(updateDimensionsSpy).not.toHaveBeenCalled();
-
- vm.$store.state.panelResizing = false;
- await nextTick();
-
- expect(updateDimensionsSpy).toHaveBeenCalledTimes(1);
-
- vm.$store.state.panelResizing = true;
- await nextTick();
-
- expect(updateDimensionsSpy).toHaveBeenCalledTimes(1);
- });
-
- it('calls updateDimensions when rightPane is toggled', async () => {
- expect(updateDimensionsSpy).not.toHaveBeenCalled();
- expect(vm.$store.state.rightPane.isOpen).toBe(false); // default value
-
- vm.$store.state.rightPane.isOpen = true;
- await nextTick();
+ it.each`
+ prefix | activeFile | viewer | shouldHaveMarkdownExtension
+ ${'Should not'} | ${dummyFile.text} | ${viewerTypes.edit} | ${false}
+ ${'Should'} | ${dummyFile.markdown} | ${viewerTypes.edit} | ${true}
+ ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.edit} | ${false}
+ ${'Should not'} | ${dummyFile.text} | ${viewerTypes.diff} | ${false}
+ ${'Should not'} | ${dummyFile.markdown} | ${viewerTypes.diff} | ${false}
+ ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.diff} | ${false}
+ ${'Should not'} | ${dummyFile.text} | ${viewerTypes.mr} | ${false}
+ ${'Should not'} | ${dummyFile.markdown} | ${viewerTypes.mr} | ${false}
+ ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.mr} | ${false}
+ `(
+ '$prefix install markdown extension for $activeFile.name in $viewer viewer',
+ async ({ activeFile, viewer, shouldHaveMarkdownExtension } = {}) => {
+ await createComponent({ state: { viewer }, activeFile });
- expect(updateDimensionsSpy).toHaveBeenCalledTimes(1);
+ if (shouldHaveMarkdownExtension) {
+ expect(applyExtensionSpy).toHaveBeenCalledWith({
+ definition: EditorMarkdownPreviewExtension,
+ setupOptions: { previewMarkdownPath: PREVIEW_MARKDOWN_PATH },
+ });
+ // TODO: spying on extensions causes Jest to blow up, so we have to assert on
+ // the public property the extension adds, as opposed to the args passed to the ctor
+ expect(wrapper.vm.editor.markdownPreview.path).toBe(PREVIEW_MARKDOWN_PATH);
+ } else {
+ expect(applyExtensionSpy).not.toHaveBeenCalledWith(
+ wrapper.vm.editor,
+ expect.any(EditorMarkdownExtension),
+ );
+ }
+ },
+ );
- vm.$store.state.rightPane.isOpen = false;
- await nextTick();
+ it('fetches the live preview extension even if markdown is not the first opened file', async () => {
+ const textFile = dummyFile.text;
+ const mdFile = dummyFile.markdown;
+ const previewExtConfig = {
+ definition: EditorMarkdownPreviewExtension,
+ setupOptions: { previewMarkdownPath: PREVIEW_MARKDOWN_PATH },
+ };
+ await createComponent({ activeFile: textFile });
+ applyExtensionSpy.mockClear();
+
+ await wrapper.setProps({ file: mdFile });
+ await waitForPromises();
- expect(updateDimensionsSpy).toHaveBeenCalledTimes(2);
+ expect(applyExtensionSpy).toHaveBeenCalledWith(previewExtConfig);
});
});
@@ -439,7 +394,6 @@ describe('RepoEditor', () => {
});
describe('files in preview mode', () => {
- let updateDimensionsSpy;
const changeViewMode = (viewMode) =>
vm.$store.dispatch('editor/updateFileEditor', {
path: vm.file.path,
@@ -451,12 +405,6 @@ describe('RepoEditor', () => {
activeFile: dummyFile.markdown,
});
- const ext = extensionsStore.get('EditorWebIde');
- updateDimensionsSpy = jest.fn();
- spyOnApi(ext, {
- updateDimensions: updateDimensionsSpy,
- });
-
changeViewMode(FILE_VIEW_MODE_PREVIEW);
await nextTick();
});
@@ -465,15 +413,6 @@ describe('RepoEditor', () => {
expect(vm.showEditor).toBe(false);
expect(findEditor().isVisible()).toBe(false);
});
-
- it('updates dimensions when switching view back to edit', async () => {
- expect(updateDimensionsSpy).not.toHaveBeenCalled();
-
- changeViewMode(FILE_VIEW_MODE_EDITOR);
- await nextTick();
-
- expect(updateDimensionsSpy).toHaveBeenCalled();
- });
});
describe('initEditor', () => {
@@ -487,7 +426,7 @@ describe('RepoEditor', () => {
it('does not fetch file information for temp entries', async () => {
await createComponent({
- activeFile: createActiveFile(),
+ activeFile: dummyFile.text,
});
expect(vm.getFileData).not.toHaveBeenCalled();
@@ -506,7 +445,7 @@ describe('RepoEditor', () => {
it('does not initialize editor for files already with content when shouldHideEditor is `true`', async () => {
await createComponent({
- activeFile: createActiveFile(),
+ activeFile: dummyFile.text,
});
await hideEditorAndRunFn();
@@ -677,9 +616,6 @@ describe('RepoEditor', () => {
activeFile: setFileName('bar.md'),
});
- vm.setupEditor();
-
- await waitForPromises();
// set cursor to line 2, column 1
vm.editor.setSelection(new Range(2, 1, 2, 1));
vm.editor.focus();
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 1939e43e5dc..0279ad454d2 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
@@ -122,7 +122,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import');
+ expect(wrapper.find(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
});
});
@@ -297,7 +297,7 @@ describe('import table', () => {
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
- expect(wrapper.text()).toContain('Showing 21-21 of 38 groups from');
+ expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from');
});
});
@@ -349,7 +349,9 @@ describe('import table', () => {
await setFilter(FILTER_VALUE);
await waitForPromises();
- expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo" from');
+ expect(wrapper.text()).toContain(
+ 'Showing 1-1 of 40 groups that you own matching filter "foo" from',
+ );
});
it('properly resets filter in graphql query when search box is cleared', async () => {
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index a556f3c17f3..356480f931e 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -85,7 +85,6 @@ describe('Incidents List', () => {
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
canCreateIncident: true,
- incidentEscalationsAvailable: true,
...provide,
},
stubs: {
@@ -211,20 +210,6 @@ describe('Incidents List', () => {
expect(status.classes('gl-text-truncate')).toBe(true);
});
});
-
- describe('when feature is disabled', () => {
- beforeEach(() => {
- mountComponent({
- data: { incidents: { list: mockIncidents }, incidentsCount },
- provide: { incidentEscalationsAvailable: false },
- loading: false,
- });
- });
-
- it('is absent if feature flag is disabled', () => {
- expect(findEscalationStatus().length).toBe(0);
- });
- });
});
it('contains a link to the incident details page', async () => {
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
index 7e24aa439d4..fae93196d2c 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
@@ -57,12 +57,12 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
</gl-button-stub>
<gl-modal-stub
+ actioncancel="[object Object]"
+ actionprimary="[object Object]"
arialabel=""
dismisslabel="Close"
modalclass=""
modalid="resetWebhookModal"
- ok-title="Reset webhook URL"
- ok-variant="danger"
size="md"
title="Reset webhook URL"
titletag="h4"
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
index d2b591d427d..521a861829b 100644
--- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -47,7 +47,7 @@ describe('Alert integration settings form', () => {
resetWebhookUrl.mockResolvedValueOnce({
data: { pagerduty_webhook_url: newWebhookUrl },
});
- findModal().vm.$emit('ok');
+ findModal().vm.$emit('primary');
await waitForPromises();
expect(resetWebhookUrl).toHaveBeenCalled();
expect(findWebhookInput().attributes('value')).toBe(newWebhookUrl);
@@ -56,7 +56,7 @@ describe('Alert integration settings form', () => {
it('should show error message and NOT reset webhook url', async () => {
resetWebhookUrl.mockRejectedValueOnce();
- findModal().vm.$emit('ok');
+ findModal().vm.$emit('primary');
await waitForPromises();
expect(findAlert().attributes('variant')).toBe('danger');
});
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index b4c5d4f9957..fa91f8de45a 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -11,7 +11,6 @@ describe('JiraIssuesFields', () => {
const defaultProps = {
showJiraVulnerabilitiesIntegration: true,
- upgradePlanPath: 'https://gitlab.com',
};
const createComponent = ({
diff --git a/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js b/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js
deleted file mode 100644
index e90e9a5d2ac..00000000000
--- a/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import JiraUpgradeCta from '~/integrations/edit/components/jira_upgrade_cta.vue';
-
-describe('JiraUpgradeCta', () => {
- let wrapper;
-
- const contentMessage = 'Upgrade your plan to enable this feature of the Jira Integration.';
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(JiraUpgradeCta, {
- propsData,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays the correct message for premium and lower users', () => {
- createComponent({ showPremiumMessage: true });
- expect(wrapper.text()).toContain('This is a Premium feature');
- expect(wrapper.text()).toContain(contentMessage);
- });
-
- it('displays the correct message for ultimate and lower users', () => {
- createComponent({ showUltimateMessage: true });
- expect(wrapper.text()).toContain('This is an Ultimate feature');
- expect(wrapper.text()).toContain(contentMessage);
- });
-});
diff --git a/spec/frontend/integrations/edit/components/sections/configuration_spec.js b/spec/frontend/integrations/edit/components/sections/configuration_spec.js
new file mode 100644
index 00000000000..e697212ea0b
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/configuration_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionCoonfiguration from '~/integrations/edit/components/sections/configuration.vue';
+import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
+import { createStore } from '~/integrations/edit/store';
+
+import { mockIntegrationProps } from '../../mock_data';
+
+describe('IntegrationSectionCoonfiguration', () => {
+ let wrapper;
+
+ const createComponent = ({ customStateProps = {}, props = {} } = {}) => {
+ const store = createStore({
+ customState: { ...mockIntegrationProps, ...customStateProps },
+ });
+ wrapper = shallowMount(IntegrationSectionCoonfiguration, {
+ propsData: { ...props },
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
+
+ describe('template', () => {
+ describe('DynamicField', () => {
+ it('renders DynamicField for each field', () => {
+ const fields = [
+ { name: 'username', type: 'text' },
+ { name: 'API token', type: 'password' },
+ ];
+
+ createComponent({
+ props: {
+ fields,
+ },
+ });
+
+ const dynamicFields = findAllDynamicFields();
+
+ expect(dynamicFields).toHaveLength(2);
+ dynamicFields.wrappers.forEach((field, index) => {
+ expect(field.props()).toMatchObject(fields[index]);
+ });
+ });
+
+ it('does not render DynamicField when field is empty', () => {
+ createComponent();
+
+ expect(findAllDynamicFields()).toHaveLength(0);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/sections/trigger_spec.js b/spec/frontend/integrations/edit/components/sections/trigger_spec.js
new file mode 100644
index 00000000000..883f5c7bf79
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/trigger_spec.js
@@ -0,0 +1,38 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionTrigger from '~/integrations/edit/components/sections/trigger.vue';
+import TriggerField from '~/integrations/edit/components/trigger_field.vue';
+import { createStore } from '~/integrations/edit/store';
+
+import { mockIntegrationProps } from '../../mock_data';
+
+describe('IntegrationSectionTrigger', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ const store = createStore({
+ customState: { ...mockIntegrationProps },
+ });
+ wrapper = shallowMount(IntegrationSectionTrigger, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findAllTriggerFields = () => wrapper.findAllComponents(TriggerField);
+
+ describe('template', () => {
+ it('renders correct number of TriggerField components', () => {
+ createComponent();
+
+ const fields = findAllTriggerFields();
+ expect(fields.length).toBe(mockIntegrationProps.triggerEvents.length);
+ fields.wrappers.forEach((field, index) => {
+ expect(field.props('event')).toBe(mockIntegrationProps.triggerEvents[index]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/trigger_field_spec.js b/spec/frontend/integrations/edit/components/trigger_field_spec.js
new file mode 100644
index 00000000000..6a68337813e
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/trigger_field_spec.js
@@ -0,0 +1,71 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlFormCheckbox } from '@gitlab/ui';
+
+import TriggerField from '~/integrations/edit/components/trigger_field.vue';
+import { integrationTriggerEventTitles } from '~/integrations/constants';
+
+describe('TriggerField', () => {
+ let wrapper;
+
+ const defaultProps = {
+ event: { name: 'push_events' },
+ };
+
+ const createComponent = ({ props = {}, isInheriting = false } = {}) => {
+ wrapper = shallowMount(TriggerField, {
+ propsData: { ...defaultProps, ...props },
+ computed: {
+ isInheriting: () => isInheriting,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findHiddenInput = () => wrapper.find('input[type="hidden"]');
+
+ describe('template', () => {
+ it('renders enabled GlFormCheckbox', () => {
+ createComponent();
+
+ expect(findGlFormCheckbox().attributes('disabled')).toBeUndefined();
+ });
+
+ it('when isInheriting is true, renders disabled GlFormCheckbox', () => {
+ createComponent({ isInheriting: true });
+
+ expect(findGlFormCheckbox().attributes('disabled')).toBe('true');
+ });
+
+ it('renders correct title', () => {
+ createComponent();
+
+ expect(findGlFormCheckbox().text()).toMatchInterpolatedText(
+ integrationTriggerEventTitles[defaultProps.event.name],
+ );
+ });
+
+ it('sets default value for hidden input', () => {
+ createComponent();
+
+ expect(findHiddenInput().attributes('value')).toBe('false');
+ });
+
+ it('toggles value of hidden input on checkbox input', async () => {
+ createComponent({
+ props: { event: { name: 'push_events', value: true } },
+ });
+ await nextTick;
+
+ expect(findHiddenInput().attributes('value')).toBe('true');
+
+ await findGlFormCheckbox().vm.$emit('input', false);
+
+ expect(findHiddenInput().attributes('value')).toBe('false');
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js
index ac0c7d244e3..c276d2e7364 100644
--- a/spec/frontend/integrations/edit/mock_data.js
+++ b/spec/frontend/integrations/edit/mock_data.js
@@ -9,7 +9,10 @@ export const mockIntegrationProps = {
initialEnableComments: false,
},
jiraIssuesProps: {},
- triggerEvents: [],
+ triggerEvents: [
+ { name: 'push_events', title: 'Push', value: true },
+ { name: 'issues_events', title: 'Issue', value: true },
+ ],
sections: [],
fields: [],
type: '',
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index 28402c8331c..c522abe63c5 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -2,7 +2,11 @@ import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
-import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '~/invite_members/constants';
+import {
+ TRIGGER_ELEMENT_BUTTON,
+ TRIGGER_ELEMENT_SIDE_NAV,
+ TRIGGER_DEFAULT_QA_SELECTOR,
+} from '~/invite_members/constants';
jest.mock('~/experimentation/experiment_tracking');
@@ -50,12 +54,24 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
wrapper.destroy();
});
- describe('displayText', () => {
+ describe('configurable attributes', () => {
it('includes the correct displayText for the button', () => {
createComponent();
expect(findButton().text()).toBe(displayText);
});
+
+ it('uses the default qa selector value', () => {
+ createComponent();
+
+ expect(findButton().attributes('data-qa-selector')).toBe(TRIGGER_DEFAULT_QA_SELECTOR);
+ });
+
+ it('sets the qa selector value', () => {
+ createComponent({ qaSelector: '_qaSelector_' });
+
+ expect(findButton().attributes('data-qa-selector')).toBe('_qaSelector_');
+ });
});
describe('clicking the link', () => {
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 010f7b999fc..cc19e90a5fa 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -200,6 +200,30 @@ describe('InviteModalBase', () => {
});
});
+ describe('when user limit is close on a personal namespace', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ closeToLimit: true,
+ reachedLimit: false,
+ usersLimitDataset: { membersPath, userNamespace: true },
+ },
+ { GlModal, GlFormGroup },
+ );
+ });
+
+ it('renders correct buttons', () => {
+ const cancelButton = findCancelButton();
+ const actionButton = findActionButton();
+
+ expect(cancelButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED);
+ expect(cancelButton.attributes('href')).toBe(membersPath);
+
+ expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT);
+ expect(actionButton.attributes('href')).toBe(); // default submit button
+ });
+ });
+
describe('when users limit is not reached', () => {
const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/;
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 4c9adbfcc44..bbc17932a49 100644
--- a/spec/frontend/invite_members/components/user_limit_notification_spec.js
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -14,9 +14,15 @@ describe('UserLimitNotification', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
- const createComponent = (reachedLimit = false, usersLimitDataset = {}) => {
+ const createComponent = (
+ closeToLimit = false,
+ reachedLimit = false,
+ usersLimitDataset = {},
+ props = {},
+ ) => {
wrapper = shallowMountExtended(UserLimitNotification, {
propsData: {
+ closeToLimit,
reachedLimit,
usersLimitDataset: {
freeUsersLimit,
@@ -25,6 +31,7 @@ describe('UserLimitNotification', () => {
purchasePath: 'purchasePath',
...usersLimitDataset,
},
+ ...props,
},
provide: { name: 'my group' },
stubs: { GlSprintf },
@@ -43,9 +50,26 @@ describe('UserLimitNotification', () => {
});
});
+ describe('when close to limit with a personal namepace', () => {
+ beforeEach(() => {
+ createComponent(true, false, { membersCount: 3, userNamespace: true });
+ });
+
+ it('renders the limit for a personal namespace', () => {
+ const alert = findAlert();
+
+ expect(alert.attributes('title')).toEqual(
+ 'You only have space for 2 more members in your personal projects',
+ );
+ expect(alert.text()).toEqual(
+ 'To make more space, you can remove members who no longer need access.',
+ );
+ });
+ });
+
describe('when close to limit', () => {
it("renders user's limit notification", () => {
- createComponent(false, { membersCount: 3 });
+ createComponent(true, false, { membersCount: 3 });
const alert = findAlert();
@@ -61,7 +85,7 @@ describe('UserLimitNotification', () => {
describe('when limit is reached', () => {
it("renders user's limit notification", () => {
- createComponent(true);
+ createComponent(true, true);
const alert = findAlert();
@@ -71,12 +95,12 @@ describe('UserLimitNotification', () => {
describe('when free user namespace', () => {
it("renders user's limit notification", () => {
- createComponent(true, { userNamespace: true });
+ createComponent(true, true, { userNamespace: true });
const alert = findAlert();
expect(alert.attributes('title')).toEqual(
- "You've reached your 5 members limit for my group",
+ "You've reached your 5 members limit for your personal projects",
);
expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE);
diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js
index ad4abda6912..f798f87b6b2 100644
--- a/spec/frontend/issuable/components/csv_export_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_export_modal_spec.js
@@ -1,7 +1,8 @@
-import { GlModal, GlIcon, GlButton } from '@gitlab/ui';
+import { GlModal, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import CsvExportModal from '~/issuable/components/csv_export_modal.vue';
+import { __ } from '~/locale';
describe('CsvExportModal', () => {
let wrapper;
@@ -34,7 +35,6 @@ describe('CsvExportModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findIcon = () => wrapper.findComponent(GlIcon);
- const findButton = () => wrapper.findComponent(GlButton);
describe('template', () => {
describe.each`
@@ -47,11 +47,25 @@ describe('CsvExportModal', () => {
});
it('displays the modal title "$modalTitle"', () => {
- expect(findModal().text()).toContain(modalTitle);
+ expect(findModal().props('title')).toBe(modalTitle);
});
- it('displays the button with title "$modalTitle"', () => {
- expect(findButton().text()).toBe(modalTitle);
+ it('displays the primary button with title "$modalTitle" and href', () => {
+ expect(findModal().props('actionPrimary')).toMatchObject({
+ text: modalTitle,
+ attributes: {
+ href: 'export/csv/path',
+ variant: 'confirm',
+ 'data-method': 'post',
+ 'data-qa-selector': `export_${issuableType}_button`,
+ 'data-track-action': 'click_button',
+ 'data-track-label': `export_${issuableType}_csv`,
+ },
+ });
+ });
+
+ it('displays the cancel button', () => {
+ expect(findModal().props('actionCancel')).toEqual({ text: __('Cancel') });
});
});
@@ -72,13 +86,5 @@ describe('CsvExportModal', () => {
);
});
});
-
- describe('primary button', () => {
- it('passes the exportCsvPath to the button', () => {
- const exportCsvPath = '/gitlab-org/gitlab-test/-/issues/export_csv';
- wrapper = createComponent({ props: { exportCsvPath } });
- expect(findButton().attributes('href')).toBe(exportCsvPath);
- });
- });
});
});
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index f4636fd7e6a..6e954c91f46 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -76,6 +76,10 @@ describe('CsvImportModal', () => {
expect(formSubmitSpy).toHaveBeenCalled();
});
+
+ it('displays the cancel button', () => {
+ expect(findModal().props('actionCancel')).toEqual({ text: __('Cancel') });
+ });
});
});
});
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
new file mode 100644
index 00000000000..3e77e750f3a
--- /dev/null
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -0,0 +1,81 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import StatusBox from '~/issuable/components/status_box.vue';
+import IssuePopover from '~/issuable/popover/components/issue_popover.vue';
+import issueQuery from '~/issuable/popover/queries/issue.query.graphql';
+
+describe('Issue Popover', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const issueQueryResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: '1',
+ issue: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/1',
+ createdAt: '2020-07-01T04:08:01Z',
+ state: 'opened',
+ title: 'Issue title',
+ },
+ },
+ },
+ };
+
+ const mountComponent = ({
+ queryResponse = jest.fn().mockResolvedValue(issueQueryResponse),
+ } = {}) => {
+ wrapper = shallowMount(IssuePopover, {
+ apolloProvider: createMockApollo([[issueQuery, queryResponse]]),
+ propsData: {
+ target: document.createElement('a'),
+ projectPath: 'foo/bar',
+ iid: '1',
+ cachedTitle: 'Cached title',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows skeleton-loader while apollo is loading', () => {
+ mountComponent();
+
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
+ });
+
+ describe('when loaded', () => {
+ beforeEach(() => {
+ mountComponent();
+ return waitForPromises();
+ });
+
+ it('shows status badge', () => {
+ expect(wrapper.findComponent(StatusBox).props()).toEqual({
+ issuableType: 'issue',
+ initialState: issueQueryResponse.data.project.issue.state,
+ });
+ });
+
+ it('shows opened time', () => {
+ expect(wrapper.text()).toContain('Opened 4 days ago');
+ });
+
+ it('shows title', () => {
+ expect(wrapper.find('h5').text()).toBe(issueQueryResponse.data.project.issue.title);
+ });
+
+ it('shows reference', () => {
+ expect(wrapper.text()).toContain('foo/bar#1');
+ });
+ });
+});
diff --git a/spec/frontend/issuable/popover/components/mr_popover_spec.js b/spec/frontend/issuable/popover/components/mr_popover_spec.js
new file mode 100644
index 00000000000..5fdd1e6e8fc
--- /dev/null
+++ b/spec/frontend/issuable/popover/components/mr_popover_spec.js
@@ -0,0 +1,119 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import MRPopover from '~/issuable/popover/components/mr_popover.vue';
+import mergeRequestQuery from '~/issuable/popover/queries/merge_request.query.graphql';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+
+describe('MR Popover', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mrQueryResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: '1',
+ mergeRequest: {
+ __typename: 'Merge Request',
+ id: 'gid://gitlab/Merge_Request/1',
+ createdAt: '2020-07-01T04:08:01Z',
+ state: 'opened',
+ title: 'MR title',
+ headPipeline: {
+ id: '1',
+ detailedStatus: {
+ id: '1',
+ icon: 'status_success',
+ group: 'success',
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const mrQueryResponseWithoutDetailedStatus = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: '1',
+ mergeRequest: {
+ __typename: 'Merge Request',
+ id: 'gid://gitlab/Merge_Request/1',
+ createdAt: '2020-07-01T04:08:01Z',
+ state: 'opened',
+ title: 'MR title',
+ headPipeline: {
+ id: '1',
+ detailedStatus: null,
+ },
+ },
+ },
+ },
+ };
+
+ const mountComponent = ({
+ queryResponse = jest.fn().mockResolvedValue(mrQueryResponse),
+ } = {}) => {
+ wrapper = shallowMount(MRPopover, {
+ apolloProvider: createMockApollo([[mergeRequestQuery, queryResponse]]),
+ propsData: {
+ target: document.createElement('a'),
+ projectPath: 'foo/bar',
+ iid: '1',
+ cachedTitle: 'Cached Title',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows skeleton-loader while apollo is loading', () => {
+ mountComponent();
+
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
+ });
+
+ describe('when loaded', () => {
+ beforeEach(() => {
+ mountComponent();
+ return waitForPromises();
+ });
+
+ it('shows opened time', () => {
+ expect(wrapper.text()).toContain('Opened 4 days ago');
+ });
+
+ it('shows title', () => {
+ expect(wrapper.find('h5').text()).toBe(mrQueryResponse.data.project.mergeRequest.title);
+ });
+
+ it('shows reference', () => {
+ expect(wrapper.text()).toContain('foo/bar!1');
+ });
+
+ it('shows CI Icon if there is pipeline data', async () => {
+ expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('without detailed status', () => {
+ beforeEach(() => {
+ mountComponent({
+ queryResponse: jest.fn().mockResolvedValue(mrQueryResponseWithoutDetailedStatus),
+ });
+ return waitForPromises();
+ });
+
+ it('does not show CI icon if there is no pipeline data', async () => {
+ expect(wrapper.findComponent(CiIcon).exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/mr_popover/index_spec.js b/spec/frontend/issuable/popover/index_spec.js
index fd8ced17aea..b1aa7f0f0b0 100644
--- a/spec/frontend/mr_popover/index_spec.js
+++ b/spec/frontend/issuable/popover/index_spec.js
@@ -1,45 +1,52 @@
import { setHTMLFixture } from 'helpers/fixtures';
import * as createDefaultClient from '~/lib/graphql';
-import initMRPopovers from '~/mr_popover/index';
+import initIssuablePopovers from '~/issuable/popover/index';
createDefaultClient.default = jest.fn();
-describe('initMRPopovers', () => {
+describe('initIssuablePopovers', () => {
let mr1;
let mr2;
let mr3;
+ let issue1;
beforeEach(() => {
setHTMLFixture(`
- <div id="one" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
+ <div id="one" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project" data-reference-type="merge_request">
MR1
</div>
- <div id="two" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
+ <div id="two" class="gfm-merge_request" title="title" data-iid="1" data-project-path="group/project" data-reference-type="merge_request">
MR2
</div>
<div id="three" class="gfm-merge_request">
MR3
</div>
+ <div id="four" class="gfm-issue" title="title" data-iid="1" data-project-path="group/project" data-reference-type="issue">
+ MR3
+ </div>
`);
mr1 = document.querySelector('#one');
mr2 = document.querySelector('#two');
mr3 = document.querySelector('#three');
+ issue1 = document.querySelector('#four');
mr1.addEventListener = jest.fn();
mr2.addEventListener = jest.fn();
mr3.addEventListener = jest.fn();
+ issue1.addEventListener = jest.fn();
});
it('does not add the same event listener twice', () => {
- initMRPopovers([mr1, mr1, mr2]);
+ initIssuablePopovers([mr1, mr1, mr2, issue1]);
expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
+ expect(issue1.addEventListener).toHaveBeenCalledTimes(1);
});
it('does not add listener if it does not have the necessary data attributes', () => {
- initMRPopovers([mr1, mr2, mr3]);
+ initIssuablePopovers([mr1, mr2, mr3]);
expect(mr3.addEventListener).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index 20b26f5abba..cb7173c56a8 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -84,7 +84,7 @@ describe('CreateMergeRequestDropdown', () => {
});
it('enables when can create confidential issue', () => {
- document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+ document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
confidentialState.selectedProject = { name: 'test' };
dropdown.enable();
@@ -93,7 +93,7 @@ describe('CreateMergeRequestDropdown', () => {
});
it('does not enable when can not create confidential issue', () => {
- document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+ document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
dropdown.enable();
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 d92ba527b5c..3f2c3c3ec5f 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -8,8 +8,6 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
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 getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
-import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -38,9 +36,11 @@ import {
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,
@@ -67,6 +67,9 @@ describe('CE IssuesListApp component', () => {
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
calendarPath: 'calendar/path',
canBulkUpdate: false,
+ canCreateProjects: false,
+ canReadCrmContact: false,
+ canReadCrmOrganization: false,
emptyStateSvgPath: 'empty-state.svg',
exportCsvPath: 'export/csv/path',
fullPath: 'path/to/project',
@@ -77,6 +80,7 @@ describe('CE IssuesListApp component', () => {
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
hasMultipleIssueAssigneesFeature: true,
+ hasScopedLabelsFeature: true,
initialEmail: 'email@example.com',
initialSort: CREATED_DESC,
isAnonymousSearchDisabled: false,
@@ -86,6 +90,7 @@ describe('CE IssuesListApp component', () => {
isSignedIn: true,
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
+ newProjectPath: 'new/project/path',
releasesPath: 'releases/path',
rssPath: 'rss/path',
showNewIssueLink: true,
@@ -100,6 +105,9 @@ describe('CE IssuesListApp component', () => {
defaultQueryResponse.data.project.issues.nodes[0].weight = 5;
}
+ const mockIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
+ const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
+
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
const findGlButton = () => wrapper.findComponent(GlButton);
@@ -113,16 +121,15 @@ describe('CE IssuesListApp component', () => {
const mountComponent = ({
provide = {},
data = {},
- issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
- issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
+ issuesQueryResponse = mockIssuesQueryResponse,
+ issuesCountsQueryResponse = mockIssuesCountsQueryResponse,
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
+ stubs = {},
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
- [getIssuesWithoutCrmQuery, issuesQueryResponse],
- [getIssuesCountsWithoutCrmQuery, issuesCountsQueryResponse],
[setSortPreferenceMutation, sortPreferenceMutationResponse],
];
@@ -136,6 +143,7 @@ describe('CE IssuesListApp component', () => {
data() {
return data;
},
+ stubs,
});
};
@@ -156,6 +164,22 @@ describe('CE IssuesListApp component', () => {
return waitForPromises();
});
+ it('queries list with types `ISSUE` and `INCIDENT', () => {
+ const expectedTypes = ['ISSUE', 'INCIDENT', 'TEST_CASE'];
+
+ expect(mockIssuesQueryResponse).toHaveBeenCalledWith(
+ expect.objectContaining({
+ types: expectedTypes,
+ }),
+ );
+
+ expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
+ expect.objectContaining({
+ types: expectedTypes,
+ }),
+ );
+ });
+
it('renders', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
@@ -301,17 +325,23 @@ describe('CE IssuesListApp component', () => {
describe('initial url params', () => {
describe('page', () => {
it('page_after is set from the url params', () => {
- setWindowLocation('?page_after=randomCursorString');
+ setWindowLocation('?page_after=randomCursorString&first_page_size=20');
wrapper = mountComponent();
- expect(wrapper.vm.$route.query).toMatchObject({ page_after: 'randomCursorString' });
+ expect(wrapper.vm.$route.query).toMatchObject({
+ page_after: 'randomCursorString',
+ first_page_size: '20',
+ });
});
it('page_before is set from the url params', () => {
- setWindowLocation('?page_before=anotherRandomCursorString');
+ setWindowLocation('?page_before=anotherRandomCursorString&last_page_size=20');
wrapper = mountComponent();
- expect(wrapper.vm.$route.query).toMatchObject({ page_before: 'anotherRandomCursorString' });
+ expect(wrapper.vm.$route.query).toMatchObject({
+ page_before: 'anotherRandomCursorString',
+ last_page_size: '20',
+ });
});
});
@@ -515,10 +545,12 @@ describe('CE IssuesListApp component', () => {
it('shows empty state', () => {
expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noIssuesSignedInDescription,
title: IssuesListApp.i18n.noIssuesSignedInTitle,
svgPath: defaultProvide.emptyStateSvgPath,
});
+ expect(findGlEmptyState().text()).toContain(
+ IssuesListApp.i18n.noIssuesSignedInDescription,
+ );
});
it('shows "New issue" and import/export buttons', () => {
@@ -532,11 +564,11 @@ describe('CE IssuesListApp component', () => {
it('shows Jira integration information', () => {
const paragraphs = wrapper.findAll('p');
- expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
- expect(paragraphs.at(2).text()).toContain(
+ expect(paragraphs.at(2).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
+ expect(paragraphs.at(3).text()).toContain(
'Enable the Jira integration to view your Jira issues in GitLab.',
);
- expect(paragraphs.at(3).text()).toContain(
+ expect(paragraphs.at(4).text()).toContain(
IssuesListApp.i18n.jiraIntegrationSecondaryMessage,
);
expect(findGlLink().text()).toBe('Enable the Jira integration');
@@ -544,6 +576,29 @@ describe('CE IssuesListApp component', () => {
});
});
+ describe('when user is logged in and can create projects', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: { canCreateProjects: true, hasAnyIssues: false, isSignedIn: true },
+ stubs: { GlEmptyState },
+ });
+ });
+
+ it('shows empty state with additional description about creating projects', () => {
+ expect(findGlEmptyState().text()).toContain(
+ IssuesListApp.i18n.noIssuesSignedInDescription,
+ );
+ expect(findGlEmptyState().text()).toContain(
+ IssuesListApp.i18n.noGroupIssuesSignedInDescription,
+ );
+ });
+
+ it('shows "New project" button', () => {
+ expect(findGlButton().text()).toBe(IssuesListApp.i18n.newProjectLabel);
+ expect(findGlButton().attributes('href')).toBe(defaultProvide.newProjectPath);
+ });
+ });
+
describe('when user is logged out', () => {
beforeEach(() => {
wrapper = mountComponent({
@@ -587,6 +642,21 @@ describe('CE IssuesListApp component', () => {
});
});
+ describe('when user does not have CRM enabled', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: { canReadCrmContact: false, canReadCrmOrganization: false },
+ });
+ });
+
+ it('does not render Contact or Organization tokens', () => {
+ expect(findIssuableList().props('searchTokens')).not.toMatchObject([
+ { type: TOKEN_TYPE_CONTACT },
+ { type: TOKEN_TYPE_ORGANIZATION },
+ ]);
+ });
+ });
+
describe('when all tokens are available', () => {
const originalGon = window.gon;
@@ -599,7 +669,13 @@ describe('CE IssuesListApp component', () => {
current_user_avatar_url: mockCurrentUser.avatar_url,
};
- wrapper = mountComponent({ provide: { isSignedIn: true } });
+ wrapper = mountComponent({
+ provide: {
+ canReadCrmContact: true,
+ canReadCrmOrganization: true,
+ isSignedIn: true,
+ },
+ });
});
afterEach(() => {
@@ -615,9 +691,11 @@ describe('CE IssuesListApp component', () => {
{ type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
{ type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
{ type: TOKEN_TYPE_CONFIDENTIAL },
+ { type: TOKEN_TYPE_CONTACT },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_MY_REACTION },
+ { type: TOKEN_TYPE_ORGANIZATION },
{ type: TOKEN_TYPE_RELEASE },
{ type: TOKEN_TYPE_TYPE },
]);
@@ -675,10 +753,10 @@ describe('CE IssuesListApp component', () => {
});
describe.each`
- event | paramName | paramValue
- ${'next-page'} | ${'page_after'} | ${'endCursor'}
- ${'previous-page'} | ${'page_before'} | ${'startCursor'}
- `('when "$event" event is emitted by IssuableList', ({ event, paramName, paramValue }) => {
+ event | params
+ ${'next-page'} | ${{ page_after: 'endCursor', page_before: undefined, first_page_size: 20, last_page_size: undefined }}
+ ${'previous-page'} | ${{ page_after: undefined, page_before: 'startCursor', first_page_size: undefined, last_page_size: 20 }}
+ `('when "$event" event is emitted by IssuableList', ({ event, params }) => {
beforeEach(() => {
wrapper = mountComponent({
data: {
@@ -697,9 +775,9 @@ describe('CE IssuesListApp component', () => {
expect(scrollUp).toHaveBeenCalled();
});
- it(`updates url with "${paramName}" param`, () => {
+ it(`updates url`, () => {
expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
- query: expect.objectContaining({ [paramName]: paramValue }),
+ query: expect.objectContaining(params),
});
});
});
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index ce0477883d7..e8ffba9bc80 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -42,27 +42,37 @@ describe('getInitialPageParams', () => {
'returns the correct page params for sort key %s with afterCursor',
(sortKey) => {
const firstPageSize = sortKey === RELATIVE_POSITION_ASC ? PAGE_SIZE_MANUAL : PAGE_SIZE;
+ const lastPageSize = undefined;
const afterCursor = 'randomCursorString';
const beforeCursor = undefined;
-
- expect(getInitialPageParams(sortKey, afterCursor, beforeCursor)).toEqual({
+ const pageParams = getInitialPageParams(
+ sortKey,
firstPageSize,
+ lastPageSize,
afterCursor,
- });
+ beforeCursor,
+ );
+
+ expect(pageParams).toEqual({ firstPageSize, afterCursor });
},
);
it.each(Object.keys(urlSortParams))(
'returns the correct page params for sort key %s with beforeCursor',
(sortKey) => {
- const firstPageSize = sortKey === RELATIVE_POSITION_ASC ? PAGE_SIZE_MANUAL : PAGE_SIZE;
+ const firstPageSize = undefined;
+ const lastPageSize = PAGE_SIZE;
const afterCursor = undefined;
const beforeCursor = 'anotherRandomCursorString';
-
- expect(getInitialPageParams(sortKey, afterCursor, beforeCursor)).toEqual({
+ const pageParams = getInitialPageParams(
+ sortKey,
firstPageSize,
+ lastPageSize,
+ afterCursor,
beforeCursor,
- });
+ );
+
+ expect(pageParams).toEqual({ lastPageSize, beforeCursor });
},
);
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 1ae04531a6b..2cc27309e59 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -17,6 +17,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import {
descriptionProps as initialProps,
@@ -370,10 +371,10 @@ describe('Description component', () => {
await findTaskLink().trigger('click');
expect(trackingSpy).toHaveBeenCalledWith(
- 'workItems:show',
+ TRACKING_CATEGORY_SHOW,
'viewed_work_item_from_modal',
{
- category: 'workItems:show',
+ category: TRACKING_CATEGORY_SHOW,
label: 'work_item_view',
property: 'type_task',
},
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 35acca60de7..8e090645be2 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -5,6 +5,7 @@ import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import DescriptionComponent from '~/issues/show/components/description.vue';
import HighlightBar from '~/issues/show/components/incidents/highlight_bar.vue';
import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue';
+import TimelineTab from '~/issues/show/components/incidents/timeline_events_tab.vue';
import INVALID_URL from '~/lib/utils/invalid_url';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
@@ -35,8 +36,9 @@ describe('Incident Tabs component', () => {
fullPath: '',
iid: '',
projectId: '',
+ issuableId: '',
uploadMetricsFeatureAvailable: true,
- glFeatures: { incidentTimeline: true, incidentTimelineEvents: true },
+ glFeatures: { incidentTimeline: true },
},
data() {
return { alert: mockAlert, ...data };
@@ -47,6 +49,9 @@ describe('Incident Tabs component', () => {
alert: {
loading: true,
},
+ timelineEvents: {
+ loading: false,
+ },
},
},
},
@@ -62,6 +67,7 @@ describe('Incident Tabs component', () => {
const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
const findHighlightBarComponent = () => wrapper.find(HighlightBar);
+ const findTimelineTab = () => wrapper.findComponent(TimelineTab);
describe('empty state', () => {
beforeEach(() => {
@@ -122,4 +128,20 @@ describe('Incident Tabs component', () => {
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
+
+ describe('incident timeline tab', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders the timeline tab when feature flag is enabled', () => {
+ expect(findTimelineTab().exists()).toBe(true);
+ });
+
+ it('does not render timeline tab when feature flag is disabled', () => {
+ mountComponent({}, { provide: { glFeatures: { incidentTimeline: false } } });
+
+ expect(findTimelineTab().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
new file mode 100644
index 00000000000..b5346a6089a
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -0,0 +1,72 @@
+export const mockEvents = [
+ {
+ action: 'comment',
+ author: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ },
+ createdAt: '2022-03-22T15:59:08Z',
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ note: 'Dummy event 1',
+ noteHtml: '<p>Dummy event 1</p>',
+ occurredAt: '2022-03-22T15:59:00Z',
+ updatedAt: '2022-03-22T15:59:08Z',
+ __typename: 'TimelineEventType',
+ },
+ {
+ action: 'comment',
+ author: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ },
+ createdAt: '2022-03-23T14:57:08Z',
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/131',
+ note: 'Dummy event 2',
+ noteHtml: '<p>Dummy event 2</p>',
+ occurredAt: '2022-03-23T14:57:00Z',
+ updatedAt: '2022-03-23T14:57:08Z',
+ __typename: 'TimelineEventType',
+ },
+ {
+ action: 'comment',
+ author: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ },
+ createdAt: '2022-03-23T15:59:08Z',
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ note: 'Dummy event 3',
+ noteHtml: '<p>Dummy event 3</p>',
+ occurredAt: '2022-03-23T15:59:00Z',
+ updatedAt: '2022-03-23T15:59:08Z',
+ __typename: 'TimelineEventType',
+ },
+];
+
+export const timelineEventsQueryListResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/8',
+ incidentManagementTimelineEvents: {
+ nodes: mockEvents,
+ },
+ },
+ },
+};
+
+export const timelineEventsQueryEmptyResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/8',
+ incidentManagementTimelineEvents: {
+ nodes: [],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js
new file mode 100644
index 00000000000..7e51219ffa7
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_item_spec.js
@@ -0,0 +1,87 @@
+import timezoneMock from 'timezone-mock';
+import merge from 'lodash/merge';
+import { GlIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
+import { mockEvents } from './mock_data';
+
+describe('IncidentTimelineEventList', () => {
+ let wrapper;
+
+ const mountComponent = (propsData) => {
+ const { action, noteHtml, occurredAt } = mockEvents[0];
+ wrapper = mountExtended(
+ IncidentTimelineEventListItem,
+ merge({
+ propsData: {
+ action,
+ noteHtml,
+ occurredAt,
+ isLastItem: false,
+ ...propsData,
+ },
+ }),
+ );
+ };
+
+ const findCommentIcon = () => wrapper.findComponent(GlIcon);
+ const findTextContainer = () => wrapper.findByTestId('event-text-container');
+ const findEventTime = () => wrapper.findByTestId('event-time');
+
+ describe('template', () => {
+ it('shows comment icon', () => {
+ mountComponent();
+
+ expect(findCommentIcon().exists()).toBe(true);
+ });
+
+ it('sets correct props for icon', () => {
+ mountComponent();
+
+ expect(findCommentIcon().props('name')).toBe(mockEvents[0].action);
+ });
+
+ it('displays the correct time', () => {
+ mountComponent();
+
+ expect(findEventTime().text()).toBe('15:59 UTC');
+ });
+
+ describe('last item in list', () => {
+ it('shows a bottom border when not the last item', () => {
+ mountComponent();
+
+ expect(findTextContainer().classes()).toContain('gl-border-1');
+ });
+
+ it('does not show a bottom border when the last item', () => {
+ mountComponent({ isLastItem: true });
+
+ expect(wrapper.classes()).not.toContain('gl-border-1');
+ });
+ });
+
+ describe.each`
+ timezone
+ ${'Europe/London'}
+ ${'US/Pacific'}
+ ${'Australia/Adelaide'}
+ `('when viewing in timezone', ({ timezone }) => {
+ describe(timezone, () => {
+ beforeEach(() => {
+ timezoneMock.register(timezone);
+
+ mountComponent();
+ });
+
+ afterEach(() => {
+ timezoneMock.unregister();
+ });
+
+ it('displays the correct time', () => {
+ expect(findEventTime().text()).toBe('15:59 UTC');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
new file mode 100644
index 00000000000..6610ea0b832
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
@@ -0,0 +1,87 @@
+import timezoneMock from 'timezone-mock';
+import merge from 'lodash/merge';
+import { shallowMountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue';
+import { mockEvents } from './mock_data';
+
+describe('IncidentTimelineEventList', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMountExtended(
+ IncidentTimelineEventList,
+ merge({
+ provide: {
+ fullPath: 'group/project',
+ issuableId: '1',
+ },
+ propsData: {
+ timelineEvents: mockEvents,
+ },
+ }),
+ );
+ };
+
+ const findGroups = () => wrapper.findAllByTestId('timeline-group');
+ const findItems = (base = wrapper) => base.findAllByTestId('timeline-event');
+ const findFirstGroup = () => extendedWrapper(findGroups().at(0));
+ const findSecondGroup = () => extendedWrapper(findGroups().at(1));
+ const findDates = () => wrapper.findAllByTestId('event-date');
+
+ describe('template', () => {
+ it('groups items correctly', () => {
+ mountComponent();
+
+ expect(findGroups()).toHaveLength(2);
+
+ expect(findItems(findFirstGroup())).toHaveLength(1);
+ expect(findItems(findSecondGroup())).toHaveLength(2);
+ });
+
+ it('sets the isLastItem prop correctly', () => {
+ mountComponent();
+
+ expect(findItems().at(0).props('isLastItem')).toBe(false);
+ expect(findItems().at(1).props('isLastItem')).toBe(false);
+ expect(findItems().at(2).props('isLastItem')).toBe(true);
+ });
+
+ it('sets the event props correctly', () => {
+ mountComponent();
+
+ expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt);
+ expect(findItems().at(1).props('action')).toBe(mockEvents[1].action);
+ expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml);
+ });
+
+ it('formats dates correctly', () => {
+ mountComponent();
+
+ expect(findDates().at(0).text()).toBe('2022-03-22');
+ expect(findDates().at(1).text()).toBe('2022-03-23');
+ });
+
+ describe.each`
+ timezone
+ ${'Europe/London'}
+ ${'US/Pacific'}
+ ${'Australia/Adelaide'}
+ `('when viewing in timezone', ({ timezone }) => {
+ describe(timezone, () => {
+ beforeEach(() => {
+ timezoneMock.register(timezone);
+
+ mountComponent();
+ });
+
+ afterEach(() => {
+ timezoneMock.unregister();
+ });
+
+ it('displays the correct time', () => {
+ expect(findDates().at(0).text()).toBe('2022-03-22');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
new file mode 100644
index 00000000000..cf81f4cdf66
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
@@ -0,0 +1,105 @@
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.vue';
+import IncidentTimelineEventsList from '~/issues/show/components/incidents/timeline_events_list.vue';
+import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createAlert } from '~/flash';
+import { timelineEventsQueryListResponse, timelineEventsQueryEmptyResponse } from './mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+const graphQLError = new Error('GraphQL error');
+const listResponse = jest.fn().mockResolvedValue(timelineEventsQueryListResponse);
+const emptyResponse = jest.fn().mockResolvedValue(timelineEventsQueryEmptyResponse);
+const errorResponse = jest.fn().mockRejectedValue(graphQLError);
+
+function createMockApolloProvider(response = listResponse) {
+ const requestHandlers = [[timelineEventsQuery, response]];
+ return createMockApollo(requestHandlers);
+}
+
+describe('TimelineEventsTab', () => {
+ let wrapper;
+
+ const mountComponent = (options = {}) => {
+ const { mockApollo, mountMethod = shallowMountExtended } = options;
+
+ wrapper = mountMethod(TimelineEventsTab, {
+ provide: {
+ fullPath: 'group/project',
+ issuableId: '1',
+ },
+ apolloProvider: mockApollo,
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList);
+
+ describe('Timeline events tab', () => {
+ describe('empty state', () => {
+ let mockApollo;
+
+ it('should show an empty list', async () => {
+ mockApollo = createMockApolloProvider(emptyResponse);
+ mountComponent({ mockApollo });
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+
+ describe('error state', () => {
+ let mockApollo;
+
+ it('should show an error state', async () => {
+ mockApollo = createMockApolloProvider(errorResponse);
+ mountComponent({ mockApollo });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: graphQLError,
+ message: 'Something went wrong while fetching incident timeline events.',
+ });
+ });
+ });
+ });
+
+ describe('timelineEventsQuery', () => {
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApolloProvider();
+ mountComponent({ mockApollo });
+ });
+
+ it('should request data', () => {
+ expect(listResponse).toHaveBeenCalled();
+ });
+
+ it('should show the loading state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(true);
+ });
+
+ it('should render the list', async () => {
+ await waitForPromises();
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTimelineEventsList().props('timelineEvents')).toHaveLength(3);
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js
new file mode 100644
index 00000000000..e6f7082d280
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/utils_spec.js
@@ -0,0 +1,31 @@
+import { displayAndLogError, getEventIcon } from '~/issues/show/components/incidents/utils';
+import { createAlert } from '~/flash';
+
+jest.mock('~/flash');
+
+describe('incident utils', () => {
+ describe('display and log error', () => {
+ it('displays and logs an error', () => {
+ const error = new Error('test');
+ displayAndLogError(error);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong while fetching incident timeline events.',
+ captureError: true,
+ error,
+ });
+ });
+ });
+
+ describe('get event icon', () => {
+ it('should display a matching event icon name', () => {
+ const name = 'comment';
+
+ expect(getEventIcon(name)).toBe(name);
+ });
+
+ it('should return a default icon name', () => {
+ expect(getEventIcon('non-existent-icon-name')).toBe('comment');
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index 8730e124ae7..8f79c74368f 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -35,7 +35,7 @@ describe('SignInOauthButton', () => {
let mockAxios;
let store;
- const createComponent = ({ slots } = {}) => {
+ const createComponent = ({ slots, props } = {}) => {
store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation();
jest.spyOn(store, 'commit').mockImplementation();
@@ -46,6 +46,7 @@ describe('SignInOauthButton', () => {
provide: {
oauthMetadata: mockOauthMetadata,
},
+ propsData: props,
});
};
@@ -65,6 +66,7 @@ describe('SignInOauthButton', () => {
expect(findButton().exists()).toBe(true);
expect(findButton().text()).toBe(I18N_DEFAULT_SIGN_IN_BUTTON_TEXT);
+ expect(findButton().props('category')).toBe('primary');
});
it.each`
@@ -208,4 +210,11 @@ describe('SignInOauthButton', () => {
});
});
});
+
+ describe('when `category` prop is set', () => {
+ it('sets the `category` prop on the GlButton', () => {
+ createComponent({ props: { category: 'tertiary' } });
+ expect(findButton().props('category')).toBe('tertiary');
+ });
+ });
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
index 2f5e47d1ae4..e16121243a0 100644
--- a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
@@ -1,5 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
+import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
+
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -16,6 +18,7 @@ describe('UserLink', () => {
provide,
stubs: {
GlSprintf,
+ SignInOauthButton,
},
});
};
@@ -23,28 +26,48 @@ describe('UserLink', () => {
const findSignInLink = () => wrapper.findByTestId('sign-in-link');
const findGitlabUserLink = () => wrapper.findByTestId('gitlab-user-link');
const findSprintf = () => wrapper.findComponent(GlSprintf);
+ const findOauthButton = () => wrapper.findComponent(SignInOauthButton);
afterEach(() => {
wrapper.destroy();
});
describe.each`
- userSignedIn | hasSubscriptions | expectGlSprintf | expectGlLink
- ${true} | ${false} | ${true} | ${false}
- ${false} | ${true} | ${false} | ${true}
- ${true} | ${true} | ${true} | ${false}
- ${false} | ${false} | ${false} | ${false}
+ userSignedIn | hasSubscriptions | expectGlSprintf | expectGlLink | expectOauthButton | jiraConnectOauthEnabled
+ ${true} | ${false} | ${true} | ${false} | ${false} | ${false}
+ ${false} | ${true} | ${false} | ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true} | ${false} | ${false} | ${false}
+ ${false} | ${false} | ${false} | ${false} | ${false} | ${false}
+ ${false} | ${true} | ${false} | ${false} | ${true} | ${true}
`(
- 'when `userSignedIn` is $userSignedIn and `hasSubscriptions` is $hasSubscriptions',
- ({ userSignedIn, hasSubscriptions, expectGlSprintf, expectGlLink }) => {
+ 'when `userSignedIn` is $userSignedIn, `hasSubscriptions` is $hasSubscriptions, `jiraConnectOauthEnabled` is $jiraConnectOauthEnabled',
+ ({
+ userSignedIn,
+ hasSubscriptions,
+ expectGlSprintf,
+ expectGlLink,
+ expectOauthButton,
+ jiraConnectOauthEnabled,
+ }) => {
it('renders template correctly', () => {
- createComponent({
- userSignedIn,
- hasSubscriptions,
- });
+ createComponent(
+ {
+ userSignedIn,
+ hasSubscriptions,
+ },
+ {
+ provide: {
+ glFeatures: {
+ jiraConnectOauth: jiraConnectOauthEnabled,
+ },
+ oauthMetadata: {},
+ },
+ },
+ );
expect(findSprintf().exists()).toBe(expectGlSprintf);
expect(findSignInLink().exists()).toBe(expectGlLink);
+ expect(findOauthButton().exists()).toBe(expectOauthButton);
});
},
);
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index 22ddc8b1c2d..2ab7f5fe22d 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -45,7 +45,7 @@ describe('Job Log Collapsible Section', () => {
});
it('renders an icon with the closed state', () => {
- expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('angle-right-icon');
+ expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-right-icon');
});
});
@@ -62,7 +62,7 @@ describe('Job Log Collapsible Section', () => {
});
it('renders an icon with the open state', () => {
- expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('angle-down-icon');
+ expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-down-icon');
});
it('renders collapsible lines content', () => {
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index 8055fe64d95..bdc8ae0eef0 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -56,8 +56,8 @@ describe('Job Log Header Line', () => {
createComponent({ ...data, isClosed: true });
});
- it('sets icon name to be angle-right', () => {
- expect(wrapper.vm.iconName).toEqual('angle-right');
+ it('sets icon name to be chevron-lg-right', () => {
+ expect(wrapper.vm.iconName).toEqual('chevron-lg-right');
});
});
@@ -66,8 +66,8 @@ describe('Job Log Header Line', () => {
createComponent({ ...data, isClosed: false });
});
- it('sets icon name to be angle-down', () => {
- expect(wrapper.vm.iconName).toEqual('angle-down');
+ it('sets icon name to be chevron-lg-down', () => {
+ expect(wrapper.vm.iconName).toEqual('chevron-lg-down');
});
});
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index 7e11738f82e..9cc56cce9b3 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -68,7 +68,9 @@ describe('Job Log', () => {
});
it('renders an icon with the open state', () => {
- expect(findCollapsibleLine().find('[data-testid="angle-down-icon"]').exists()).toBe(true);
+ expect(findCollapsibleLine().find('[data-testid="chevron-lg-down-icon"]').exists()).toBe(
+ true,
+ );
});
describe('on click header section', () => {
@@ -146,7 +148,9 @@ describe('Job Log, infinitelyCollapsibleSections feature flag enabled', () => {
});
it('renders an icon with the open state', () => {
- expect(findCollapsibleLine().find('[data-testid="angle-down-icon"]').exists()).toBe(true);
+ expect(findCollapsibleLine().find('[data-testid="chevron-lg-down-icon"]').exists()).toBe(
+ true,
+ );
});
describe('on click header section', () => {
diff --git a/spec/frontend/labels/delete_label_modal_spec.js b/spec/frontend/labels/delete_label_modal_spec.js
index 98049538948..67220821fe0 100644
--- a/spec/frontend/labels/delete_label_modal_spec.js
+++ b/spec/frontend/labels/delete_label_modal_spec.js
@@ -25,11 +25,11 @@ describe('DeleteLabelModal', () => {
buttons.forEach((x) => {
const button = document.createElement('button');
button.setAttribute('class', 'js-delete-label-modal-button');
- button.setAttribute('data-label-name', x.labelName);
- button.setAttribute('data-destroy-path', x.destroyPath);
+ button.dataset.labelName = x.labelName;
+ button.dataset.destroyPath = x.destroyPath;
if (x.subjectName) {
- button.setAttribute('data-subject-name', x.subjectName);
+ button.dataset.subjectName = x.subjectName;
}
button.innerHTML = 'Action';
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index 3d8b0d9c307..e0b6c7119f9 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -27,7 +27,7 @@ describe('LazyLoader', () => {
const createLazyLoadImage = () => {
const newImg = document.createElement('img');
newImg.className = 'lazy';
- newImg.setAttribute('data-src', TEST_PATH);
+ newImg.dataset.src = TEST_PATH;
document.body.appendChild(newImg);
triggerChildMutation();
@@ -108,7 +108,7 @@ describe('LazyLoader', () => {
expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(TEST_PATH);
- expect(img.getAttribute('data-src')).toBe(null);
+ expect(img.dataset.src).toBeUndefined();
expect(img).toHaveClass('js-lazy-loaded');
});
diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js
index c9a480e9943..7aab0072364 100644
--- a/spec/frontend/lib/gfm/index_spec.js
+++ b/spec/frontend/lib/gfm/index_spec.js
@@ -1,35 +1,48 @@
import { render } from '~/lib/gfm';
describe('gfm', () => {
+ const markdownToAST = async (markdown) => {
+ let result;
+
+ await render({
+ markdown,
+ renderer: (tree) => {
+ result = tree;
+ },
+ });
+
+ return result;
+ };
+
+ const expectInRoot = (result, ...nodes) => {
+ expect(result).toEqual(
+ expect.objectContaining({
+ children: expect.arrayContaining(nodes),
+ }),
+ );
+ };
+
describe('render', () => {
it('processes Commonmark and provides an ast to the renderer function', async () => {
- let result;
-
- await render({
- markdown: 'This is text',
- renderer: (tree) => {
- result = tree;
- },
- });
+ const result = await markdownToAST('This is text');
expect(result.type).toBe('root');
});
it('transforms raw HTML into individual nodes in the AST', async () => {
- let result;
-
- await render({
- markdown: '<strong>This is bold text</strong>',
- renderer: (tree) => {
- result = tree;
- },
- });
+ const result = await markdownToAST('<strong>This is bold text</strong>');
- expect(result.children[0].children[0]).toMatchObject({
- type: 'element',
- tagName: 'strong',
- properties: {},
- });
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'strong',
+ }),
+ ]),
+ }),
+ );
});
it('returns the result of executing the renderer function', async () => {
@@ -44,5 +57,40 @@ describe('gfm', () => {
expect(result).toEqual(rendered);
});
+
+ it('transforms footnotes into footnotedefinition and footnotereference tags', async () => {
+ const result = await markdownToAST(
+ `footnote reference [^footnote]
+
+[^footnote]: Footnote definition`,
+ );
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ children: expect.arrayContaining([
+ expect.objectContaining({
+ type: 'element',
+ tagName: 'footnotereference',
+ properties: {
+ identifier: 'footnote',
+ label: 'footnote',
+ },
+ }),
+ ]),
+ }),
+ );
+
+ expectInRoot(
+ result,
+ expect.objectContaining({
+ tagName: 'footnotedefinition',
+ properties: {
+ identifier: 'footnote',
+ label: 'footnote',
+ },
+ }),
+ );
+ });
});
});
diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index 88dac449527..b537e6b2bf8 100644
--- a/spec/frontend/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -5,7 +5,6 @@ import {
canScrollDown,
parseBooleanDataAttributes,
isElementVisible,
- isElementHidden,
getParents,
getParentByTagName,
setAttributes,
@@ -181,30 +180,21 @@ describe('DOM Utils', () => {
${1} | ${0} | ${0} | ${true}
${0} | ${1} | ${0} | ${true}
${0} | ${0} | ${1} | ${true}
- `(
- 'isElementVisible and isElementHidden',
- ({ offsetWidth, offsetHeight, clientRectsLength, visible }) => {
- const element = {
- offsetWidth,
- offsetHeight,
- getClientRects: () => new Array(clientRectsLength),
- };
-
- const paramDescription = `offsetWidth=${offsetWidth}, offsetHeight=${offsetHeight}, and getClientRects().length=${clientRectsLength}`;
-
- describe('isElementVisible', () => {
- it(`returns ${visible} when ${paramDescription}`, () => {
- expect(isElementVisible(element)).toBe(visible);
- });
+ `('isElementVisible', ({ offsetWidth, offsetHeight, clientRectsLength, visible }) => {
+ const element = {
+ offsetWidth,
+ offsetHeight,
+ getClientRects: () => new Array(clientRectsLength),
+ };
+
+ const paramDescription = `offsetWidth=${offsetWidth}, offsetHeight=${offsetHeight}, and getClientRects().length=${clientRectsLength}`;
+
+ describe('isElementVisible', () => {
+ it(`returns ${visible} when ${paramDescription}`, () => {
+ expect(isElementVisible(element)).toBe(visible);
});
-
- describe('isElementHidden', () => {
- it(`returns ${!visible} when ${paramDescription}`, () => {
- expect(isElementHidden(element)).toBe(!visible);
- });
- });
- },
- );
+ });
+ });
describe('getParents', () => {
it('gets all parents of an element', () => {
diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js
index 123d36ac5d5..2f71b26b29a 100644
--- a/spec/frontend/lib/utils/forms_spec.js
+++ b/spec/frontend/lib/utils/forms_spec.js
@@ -157,7 +157,7 @@ describe('lib/utils/forms', () => {
mountEl.innerHTML = `
<input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name">
<input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail">
- <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contact_info_phone">
+ <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" maxlength="12" pattern="mockPattern" data-js-name="contact_info_phone">
<input type="hidden" placeholder="Job title" value="" name="user[job_title]" id="user_job_title" data-js-name="jobTitle">
<textarea name="user[bio]" id="user_bio" data-js-name="bio">Foo bar</textarea>
<select name="user[timezone]" id="user_timezone" data-js-name="timezone">
@@ -192,6 +192,8 @@ describe('lib/utils/forms', () => {
id: 'user_contact_info_phone',
value: '(123) 456-7890',
placeholder: 'Phone',
+ maxLength: 12,
+ pattern: 'mockPattern',
},
jobTitle: {
name: 'user[job_title]',
diff --git a/spec/frontend/lib/utils/rails_ujs_spec.js b/spec/frontend/lib/utils/rails_ujs_spec.js
new file mode 100644
index 00000000000..00c29b72e73
--- /dev/null
+++ b/spec/frontend/lib/utils/rails_ujs_spec.js
@@ -0,0 +1,78 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+
+beforeAll(async () => {
+ // @rails/ujs expects jQuery.ajaxPrefilter to exist if jQuery exists at
+ // import time. This is only a problem in tests, since we expose jQuery
+ // globally earlier than in production builds. Work around this by pretending
+ // that jQuery isn't available *before* we import @rails/ujs.
+ delete global.jQuery;
+
+ const { initRails } = await import('~/lib/utils/rails_ujs.js');
+ initRails();
+});
+
+function mockXHRResponse({ responseText, responseContentType } = {}) {
+ jest
+ .spyOn(global.XMLHttpRequest.prototype, 'getResponseHeader')
+ .mockReturnValue(responseContentType);
+
+ jest.spyOn(global.XMLHttpRequest.prototype, 'send').mockImplementation(function send() {
+ requestAnimationFrame(() => {
+ Object.defineProperties(this, {
+ readyState: { value: XMLHttpRequest.DONE },
+ status: { value: 200 },
+ response: { value: responseText },
+ });
+ this.onreadystatechange();
+ });
+ });
+}
+
+// This is a test to make sure that the patch-package patch correctly disables
+// script execution for data-remote attributes.
+it('does not perform script execution via data-remote', async () => {
+ global.scriptExecutionSpy = jest.fn();
+
+ mockXHRResponse({
+ responseText: 'scriptExecutionSpy();',
+ responseContentType: 'application/javascript',
+ });
+
+ setHTMLFixture(`
+ <a href="/foo/evil.js"
+ data-remote="true"
+ data-method="get"
+ data-type="script"
+ data-testid="evil-link"
+ >XSS</a>
+ `);
+
+ const link = document.querySelector('[data-testid="evil-link"]');
+ const ajaxSuccessSpy = jest.fn();
+ link.addEventListener('ajax:success', ajaxSuccessSpy);
+
+ link.click();
+
+ await waitForPromises();
+
+ // Make sure Rails ajax machinery finished working as expected to avoid false
+ // positives
+ expect(ajaxSuccessSpy).toHaveBeenCalledTimes(1);
+
+ // If @rails/ujs has been patched correctly, this next assertion should pass.
+ //
+ // Because it's asserting something didn't happen, it is possible for it to
+ // pass for the wrong reason. So, to verify that this test correctly fails
+ // when @rails/ujs has not been patched, run:
+ //
+ // yarn patch-package --reverse
+ //
+ // And then re-run this test. The spy should now be called, and correctly
+ // fail the test.
+ //
+ // To restore the patch(es), run:
+ //
+ // yarn install
+ expect(global.scriptExecutionSpy).not.toHaveBeenCalled();
+});
diff --git a/spec/frontend/lib/utils/table_utility_spec.js b/spec/frontend/lib/utils/table_utility_spec.js
index 0ceccbe4c74..df9006f4909 100644
--- a/spec/frontend/lib/utils/table_utility_spec.js
+++ b/spec/frontend/lib/utils/table_utility_spec.js
@@ -9,6 +9,13 @@ describe('table_utility', () => {
});
});
+ describe('thWidthPercent', () => {
+ it('returns the width class including default table header classes', () => {
+ const width = 50;
+ expect(tableUtils.thWidthPercent(width)).toBe(`gl-w-${width}p`);
+ });
+ });
+
describe('sortObjectToString', () => {
it('returns the expected sorting string ending in "DESC" when sortDesc is true', () => {
expect(tableUtils.sortObjectToString({ sortBy: 'mergedAt', sortDesc: true })).toBe(
diff --git a/spec/frontend/lib/utils/users_cache_spec.js b/spec/frontend/lib/utils/users_cache_spec.js
index d35ba20f570..5a55874b5fa 100644
--- a/spec/frontend/lib/utils/users_cache_spec.js
+++ b/spec/frontend/lib/utils/users_cache_spec.js
@@ -154,8 +154,8 @@ describe('UsersCache', () => {
};
const user = await UsersCache.retrieveById(dummyUserId);
- expect(user).toBe(dummyUser);
- expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser);
+ expect(user).toEqual(dummyUser);
+ expect(UsersCache.internalStorage[dummyUserId]).toEqual(dummyUser);
});
it('returns undefined if Ajax call fails and cache is empty', async () => {
@@ -180,6 +180,29 @@ describe('UsersCache', () => {
const user = await UsersCache.retrieveById(dummyUserId);
expect(user).toBe(dummyUser);
});
+
+ it('does not clobber existing cached values', async () => {
+ UsersCache.internalStorage[dummyUserId] = {
+ status: dummyUserStatus,
+ };
+
+ apiSpy = (id) => {
+ expect(id).toBe(dummyUserId);
+
+ return Promise.resolve({
+ data: dummyUser,
+ });
+ };
+
+ const user = await UsersCache.retrieveById(dummyUserId);
+ const expectedUser = {
+ status: dummyUserStatus,
+ ...dummyUser,
+ };
+
+ expect(user).toEqual(expectedUser);
+ expect(UsersCache.internalStorage[dummyUserId]).toEqual(expectedUser);
+ });
});
describe('retrieveStatusById', () => {
diff --git a/spec/frontend/logs/utils_spec.js b/spec/frontend/logs/utils_spec.js
deleted file mode 100644
index 986fe320363..00000000000
--- a/spec/frontend/logs/utils_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { getTimeRange } from '~/logs/utils';
-
-describe('logs/utils', () => {
- describe('getTimeRange', () => {
- const nowTimestamp = 1577836800000;
- const nowString = '2020-01-01T00:00:00.000Z';
-
- beforeEach(() => {
- jest.spyOn(Date, 'now').mockImplementation(() => nowTimestamp);
- });
-
- afterEach(() => {
- Date.now.mockRestore();
- });
-
- it('returns the right values', () => {
- expect(getTimeRange(0)).toEqual({
- start: '2020-01-01T00:00:00.000Z',
- end: nowString,
- });
-
- expect(getTimeRange(60 * 30)).toEqual({
- start: '2019-12-31T23:30:00.000Z',
- end: nowString,
- });
-
- expect(getTimeRange(60 * 60 * 24 * 7 * 1)).toEqual({
- start: '2019-12-25T00:00:00.000Z',
- end: nowString,
- });
-
- expect(getTimeRange(60 * 60 * 24 * 7 * 4)).toEqual({
- start: '2019-12-04T00:00:00.000Z',
- end: nowString,
- });
- });
- });
-});
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 1d882e5ef09..1354b938d77 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -9,6 +9,7 @@ import {
MEMBER_TYPES,
TAB_QUERY_PARAM_VALUES,
ACTIVE_TAB_QUERY_PARAM_NAME,
+ FILTERED_SEARCH_TOKEN_GROUPS_WITH_INHERITED_PERMISSIONS,
} from '~/members/constants';
import { pagination } from '../mock_data';
@@ -42,6 +43,7 @@ describe('MembersTabs', () => {
},
filteredSearchBar: {
searchParam: 'search_groups',
+ tokens: [FILTERED_SEARCH_TOKEN_GROUPS_WITH_INHERITED_PERMISSIONS.type],
},
},
},
@@ -163,6 +165,18 @@ describe('MembersTabs', () => {
expect(findTabByText('Groups')).not.toBeUndefined();
});
});
+
+ describe('when url param matches `filteredSearchBar.tokens`', () => {
+ beforeEach(() => {
+ setWindowLocation('?groups_with_inherited_permissions=exclude');
+ });
+
+ it('shows tab that corresponds to filtered search token', async () => {
+ await createComponent({ totalItems: 0 });
+
+ expect(findTabByText('Groups')).not.toBeUndefined();
+ });
+ });
});
describe('when `canManageMembers` is `false`', () => {
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 298a01e4f4d..08baa663bf0 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -16,12 +16,11 @@ import {
MEMBER_STATE_CREATED,
MEMBER_STATE_AWAITING,
MEMBER_STATE_ACTIVE,
- USER_STATE_BLOCKED_PENDING_APPROVAL,
- BADGE_LABELS_AWAITING_USER_SIGNUP,
- BADGE_LABELS_PENDING_OWNER_APPROVAL,
+ USER_STATE_BLOCKED,
+ BADGE_LABELS_AWAITING_SIGNUP,
+ BADGE_LABELS_PENDING,
TAB_QUERY_PARAM_VALUES,
} from '~/members/constants';
-import * as initUserPopovers from '~/user_popovers';
import {
member as memberMock,
directMember,
@@ -134,14 +133,14 @@ describe('MembersTable', () => {
describe('Invited column', () => {
describe.each`
- state | userState | expectedBadgeLabel
- ${MEMBER_STATE_CREATED} | ${null} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
- ${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
- ${MEMBER_STATE_AWAITING} | ${''} | ${BADGE_LABELS_AWAITING_USER_SIGNUP}
- ${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED_PENDING_APPROVAL} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
- ${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING_OWNER_APPROVAL}
- ${MEMBER_STATE_ACTIVE} | ${null} | ${''}
- ${MEMBER_STATE_ACTIVE} | ${'something_else'} | ${''}
+ state | userState | expectedBadgeLabel
+ ${MEMBER_STATE_CREATED} | ${null} | ${BADGE_LABELS_AWAITING_SIGNUP}
+ ${MEMBER_STATE_CREATED} | ${USER_STATE_BLOCKED} | ${BADGE_LABELS_PENDING}
+ ${MEMBER_STATE_AWAITING} | ${''} | ${BADGE_LABELS_AWAITING_SIGNUP}
+ ${MEMBER_STATE_AWAITING} | ${USER_STATE_BLOCKED} | ${BADGE_LABELS_PENDING}
+ ${MEMBER_STATE_AWAITING} | ${'something_else'} | ${BADGE_LABELS_PENDING}
+ ${MEMBER_STATE_ACTIVE} | ${null} | ${''}
+ ${MEMBER_STATE_ACTIVE} | ${'something_else'} | ${''}
`('Invited Badge', ({ state, userState, expectedBadgeLabel }) => {
it(`${
expectedBadgeLabel ? 'shows' : 'hides'
@@ -257,14 +256,6 @@ describe('MembersTable', () => {
});
});
- it('initializes user popovers when mounted', () => {
- const initUserPopoversMock = jest.spyOn(initUserPopovers, 'default');
-
- createComponent();
-
- expect(initUserPopoversMock).toHaveBeenCalled();
- });
-
it('adds QA selector to table', () => {
createComponent();
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index efabe54f238..251a8b0b774 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -24,7 +24,7 @@ describe('initMembersApp', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-members-data', dataAttribute);
+ el.dataset.membersData = dataAttribute;
window.gon = { current_user_id: 123 };
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index a157cfa1c1d..b0c9459ff4f 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -256,7 +256,7 @@ describe('Members Utils', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-members-data', dataAttribute);
+ el.dataset.membersData = dataAttribute;
});
afterEach(() => {
diff --git a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
index 55e666609bd..4fdc4024e10 100644
--- a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
+++ b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
@@ -59,7 +59,7 @@ describe('Merge Conflict Resolver App', () => {
const title = findConflictsCount();
expect(title.exists()).toBe(true);
- expect(title.text().trim()).toBe('Showing 3 conflicts between test-conflicts and main');
+ expect(title.text().trim()).toBe('Showing 3 conflicts');
});
it('shows a loading spinner while loading', () => {
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index ccbc61ea658..f0f051cbc8b 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -325,6 +325,28 @@ describe('MergeRequestTabs', () => {
expect(window.scrollTo.mock.calls[0]).toEqual([0, 39]);
});
+ it.each`
+ tab | hides | hidesText
+ ${'show'} | ${false} | ${'shows'}
+ ${'diffs'} | ${true} | ${'hides'}
+ ${'commits'} | ${true} | ${'hides'}
+ `('it $hidesText expand button on $tab tab', ({ tab, hides }) => {
+ const expandButton = document.createElement('div');
+ expandButton.classList.add('js-expand-sidebar');
+
+ const tabsContainer = document.createElement('div');
+ tabsContainer.innerHTML =
+ '<div class="tab-content"><div id="diff-notes-app"></div><div class="commits tab-pane"></div></div>';
+ tabsContainer.classList.add('merge-request-tabs-container');
+ tabsContainer.appendChild(expandButton);
+ document.body.appendChild(tabsContainer);
+
+ testContext.class = new MergeRequestTabs({ stubLocation });
+ testContext.class.tabShown(tab, 'foobar');
+
+ expect(testContext.class.expandSidebar.classList.contains('gl-display-none!')).toBe(hides);
+ });
+
describe('when switching tabs', () => {
const SCROLL_TOP = 100;
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index a93035cc53a..a9f37f90561 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -151,7 +151,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
emptynodatasvgpath="/images/illustrations/monitoring/no_data.svg"
emptyunabletoconnectsvgpath="/images/illustrations/monitoring/unable_to_connect.svg"
selectedstate="gettingStarted"
- settingspath="/monitoring/monitor-project/-/integrations/prometheus/edit"
+ settingspath="/monitoring/monitor-project/-/settings/integrations/prometheus/edit"
/>
</div>
`;
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index c5b45564089..31f52f6627b 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -34,17 +34,17 @@ describe('Graph group component', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('should show the angle-down caret icon', () => {
+ it('should show the chevron-lg-down caret icon', () => {
expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().props('name')).toBe('angle-down');
+ expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
});
- it('should show the angle-right caret icon when the user collapses the group', async () => {
+ it('should show the chevron-lg-right caret icon when the user collapses the group', async () => {
findToggleButton().trigger('click');
await nextTick();
expect(findContent().isVisible()).toBe(false);
- expect(findCaretIcon().props('name')).toBe('angle-right');
+ expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
});
it('should contain a tab index for the collapse button', () => {
@@ -60,7 +60,7 @@ describe('Graph group component', () => {
await nextTick();
expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().props('name')).toBe('angle-down');
+ expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
});
});
@@ -72,15 +72,15 @@ describe('Graph group component', () => {
});
});
- it('should show the angle-down caret icon when collapseGroup is true', () => {
- expect(findCaretIcon().props('name')).toBe('angle-right');
+ it('should show the chevron-lg-down caret icon when collapseGroup is true', () => {
+ expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
});
- it('should show the angle-right caret icon when collapseGroup is false', async () => {
+ it('should show the chevron-lg-right caret icon when collapseGroup is false', async () => {
findToggleButton().trigger('click');
await nextTick();
- expect(findCaretIcon().props('name')).toBe('angle-down');
+ expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
});
it('should call collapse the graph group content when enter is pressed on the caret icon', () => {
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
index 6a19815883a..f4062adea81 100644
--- a/spec/frontend/monitoring/fixture_data.js
+++ b/spec/frontend/monitoring/fixture_data.js
@@ -14,13 +14,12 @@ const datasetState = stateAndPropsFromDataset(
convertObjectPropsToCamelCase(metricsDashboardResponse.metrics_data),
);
-// new properties like addDashboardDocumentationPath prop and alertsEndpoint
+// new properties like addDashboardDocumentationPath prop
// was recently added to dashboard.vue component this needs to be
// added to fixtures data
// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
export const dashboardProps = {
...datasetState.dataProps,
- alertsEndpoint: null,
};
export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload);
diff --git a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
deleted file mode 100644
index 5d84b4660c9..00000000000
--- a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
+++ /dev/null
@@ -1,91 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MR Popover loaded state matches the snapshot 1`] = `
-<gl-popover-stub
- boundary="viewport"
- cssclasses=""
- placement="top"
- show=""
- target=""
->
- <div
- class="mr-popover"
- >
- <div
- class="d-flex align-items-center justify-content-between"
- >
- <div
- class="d-inline-flex align-items-center"
- >
- <div
- class="issuable-status-box status-box status-box-open"
- >
-
- Open
-
- </div>
-
- <span
- class="gl-text-secondary"
- >
- Opened
- <time>
- just now
- </time>
- </span>
- </div>
-
- <ci-icon-stub
- cssclasses=""
- size="16"
- status="[object Object]"
- />
- </div>
-
- <h5
- class="my-2"
- >
- Updated Title
- </h5>
-
- <div
- class="gl-text-secondary"
- >
-
- foo/bar!1
-
- </div>
- </div>
-</gl-popover-stub>
-`;
-
-exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = `
-<gl-popover-stub
- boundary="viewport"
- cssclasses=""
- placement="top"
- show=""
- target=""
->
- <div
- class="mr-popover"
- >
- <div>
- <gl-skeleton-loading-stub
- class="animation-container-small mt-1"
- lines="1"
- />
- </div>
-
- <!---->
-
- <div
- class="gl-text-secondary"
- >
-
- foo/bar!1
-
- </div>
- </div>
-</gl-popover-stub>
-`;
diff --git a/spec/frontend/mr_popover/mr_popover_spec.js b/spec/frontend/mr_popover/mr_popover_spec.js
deleted file mode 100644
index 23f97073e9e..00000000000
--- a/spec/frontend/mr_popover/mr_popover_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import MRPopover from '~/mr_popover/components/mr_popover.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-
-describe('MR Popover', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallowMount(MRPopover, {
- propsData: {
- target: document.createElement('a'),
- projectPath: 'foo/bar',
- mergeRequestIID: '1',
- mergeRequestTitle: 'MR Title',
- },
- mocks: {
- $apollo: {
- queries: {
- mergeRequest: {
- loading: false,
- },
- },
- },
- },
- });
- });
-
- it('shows skeleton-loader while apollo is loading', async () => {
- wrapper.vm.$apollo.queries.mergeRequest.loading = true;
-
- await nextTick();
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('loaded state', () => {
- it('matches the snapshot', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- mergeRequest: {
- title: 'Updated Title',
- state: 'opened',
- createdAt: new Date(),
- headPipeline: {
- detailedStatus: {
- group: 'success',
- status: 'status_success',
- },
- },
- },
- });
-
- await nextTick();
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('does not show CI Icon if there is no pipeline data', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- mergeRequest: {
- state: 'opened',
- headPipeline: null,
- stateHumanName: 'Open',
- title: 'Merge Request Title',
- createdAt: new Date(),
- },
- });
-
- await nextTick();
- expect(wrapper.find(CiIcon).exists()).toBe(false);
- });
-
- it('falls back to cached MR title when request fails', async () => {
- await nextTick();
- expect(wrapper.text()).toContain('MR Title');
- });
- });
-});
diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js
index 937c44727c7..f87de0afb14 100644
--- a/spec/frontend/nav/components/responsive_header_spec.js
+++ b/spec/frontend/nav/components/responsive_header_spec.js
@@ -43,7 +43,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItem: {
id: 'home',
view: 'home',
- icon: 'angle-left',
+ icon: 'chevron-lg-left',
},
iconOnly: true,
});
@@ -60,7 +60,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
it('emits menu-item-click', () => {
expect(wrapper.emitted()).toEqual({
- 'menu-item-click': [[{ id: 'home', view: 'home', icon: 'angle-left' }]],
+ 'menu-item-click': [[{ id: 'home', view: 'home', icon: 'chevron-lg-left' }]],
});
});
});
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index 7dc6f90d202..de415b5bfe0 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -78,8 +78,8 @@ describe('Markdown component', () => {
});
await nextTick();
- expect(findLink().getAttribute('data-remote')).toBe(null);
- expect(findLink().getAttribute('data-type')).toBe(null);
+ expect(findLink().dataset.remote).toBeUndefined();
+ expect(findLink().dataset.type).toBeUndefined();
});
describe('When parsing images', () => {
diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js
index 90c989540b9..d69c2c4adfa 100644
--- a/spec/frontend/notes/components/comment_field_layout_spec.js
+++ b/spec/frontend/notes/components/comment_field_layout_spec.js
@@ -135,14 +135,14 @@ describe('Comment Field Layout Component', () => {
});
});
- describe('issue has email participants, but note is confidential', () => {
+ describe('issue has email participants, but note is internal', () => {
it('does not show EmailParticipantsWarning', () => {
createWrapper({
noteableData: {
...noteableDataMock,
issue_email_participants: [{ email: 'someone@gitlab.com' }],
},
- noteIsConfidential: true,
+ isInternalNote: true,
});
expect(findEmailParticipantsWarning().exists()).toBe(false);
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index ba5d4d27e55..116016ecae2 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -32,7 +32,7 @@ describe('issue_comment_form component', () => {
const findTextArea = () => wrapper.findByTestId('comment-field');
const findAddToReviewButton = () => wrapper.findByTestId('add-to-review-button');
const findAddCommentNowButton = () => wrapper.findByTestId('add-comment-now-button');
- const findConfidentialNoteCheckbox = () => wrapper.findByTestId('confidential-note-checkbox');
+ const findConfidentialNoteCheckbox = () => wrapper.findByTestId('internal-note-checkbox');
const findCommentTypeDropdown = () => wrapper.findComponent(CommentTypeDropdown);
const findCommentButton = () => findCommentTypeDropdown().find('button');
const findErrorAlerts = () => wrapper.findAllComponents(GlAlert).wrappers;
@@ -249,15 +249,15 @@ describe('issue_comment_form component', () => {
describe('textarea', () => {
describe('general', () => {
it.each`
- noteType | confidential | placeholder
- ${'comment'} | ${false} | ${'Write a comment or drag your files here…'}
- ${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
+ noteType | noteIsInternal | placeholder
+ ${'comment'} | ${false} | ${'Write a comment or drag your files here…'}
+ ${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
`(
'should render textarea with placeholder for $noteType',
- ({ confidential, placeholder }) => {
+ ({ noteIsInternal, placeholder }) => {
mountComponent({
mountFunction: mount,
- initialData: { noteIsConfidential: confidential },
+ initialData: { noteIsInternal },
});
expect(findTextArea().attributes('placeholder')).toBe(placeholder);
@@ -389,14 +389,14 @@ describe('issue_comment_form component', () => {
});
it.each`
- confidential | buttonText
- ${false} | ${'Comment'}
- ${true} | ${'Add internal note'}
- `('renders comment button with text "$buttonText"', ({ confidential, buttonText }) => {
+ noteIsInternal | buttonText
+ ${false} | ${'Comment'}
+ ${true} | ${'Add internal note'}
+ `('renders comment button with text "$buttonText"', ({ noteIsInternal, buttonText }) => {
mountComponent({
mountFunction: mount,
- noteableData: createNotableDataMock({ confidential }),
- initialData: { noteIsConfidential: confidential },
+ noteableData: createNotableDataMock({ confidential: noteIsInternal }),
+ initialData: { noteIsInternal },
});
expect(findCommentButton().text()).toBe(buttonText);
@@ -487,8 +487,8 @@ describe('issue_comment_form component', () => {
await findCloseReopenButton().trigger('click');
- await nextTick;
- await nextTick;
+ await nextTick();
+ await nextTick();
expect(createFlash).toHaveBeenCalledWith({
message: `Something went wrong while closing the ${type}. Please try again later.`,
@@ -523,8 +523,8 @@ describe('issue_comment_form component', () => {
await findCloseReopenButton().trigger('click');
- await nextTick;
- await nextTick;
+ await nextTick();
+ await nextTick();
expect(createFlash).toHaveBeenCalledWith({
message: `Something went wrong while reopening the ${type}. Please try again later.`,
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 378dcb97fab..0f765a8da87 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { suggestionCommitMessage } from '~/diffs/store/getters';
import NoteBody from '~/notes/components/note_body.vue';
@@ -7,6 +7,7 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
+import { INTERNAL_NOTE_CLASSES } from '~/notes/constants';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
@@ -27,7 +28,7 @@ const createComponent = ({
mockStore.dispatch('setNotesData', notesData);
}
- return shallowMount(NoteBody, {
+ return shallowMountExtended(NoteBody, {
store: mockStore || store,
propsData: {
note,
@@ -58,6 +59,24 @@ describe('issue_note_body component', () => {
expect(wrapper.findComponent(NoteAwardsList).exists()).toBe(true);
});
+ it('should not have internal note classes', () => {
+ expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual(
+ INTERNAL_NOTE_CLASSES,
+ );
+ });
+
+ describe('isInternalNote', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ props: { isInternalNote: true } });
+ });
+
+ it('should have internal note classes', () => {
+ expect(wrapper.findByTestId('note-internal-container').classes()).toEqual(
+ INTERNAL_NOTE_CLASSES,
+ );
+ });
+ });
+
describe('isEditing', () => {
beforeEach(() => {
wrapper = createComponent({ props: { isEditing: true } });
@@ -86,6 +105,18 @@ describe('issue_note_body component', () => {
// which is defined in `app/assets/javascripts/notes/mixins/autosave.js`
expect(wrapper.vm.autosave.key).toEqual(autosaveKey);
});
+
+ describe('isInternalNote', () => {
+ beforeEach(() => {
+ wrapper.setProps({ isInternalNote: true });
+ });
+
+ it('should not have internal note classes', () => {
+ expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual(
+ INTERNAL_NOTE_CLASSES,
+ );
+ });
+ });
});
describe('commitMessage', () => {
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 310a470aa18..ad2cf1c5a35 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -21,7 +21,7 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
- const findConfidentialIndicator = () => wrapper.findByTestId('internalNoteIndicator');
+ const findInternalNoteIndicator = () => wrapper.findByTestId('internalNoteIndicator');
const findSpinner = () => wrapper.find({ ref: 'spinner' });
const findAuthorStatus = () => wrapper.find({ ref: 'authorStatus' });
@@ -283,20 +283,20 @@ describe('NoteHeader component', () => {
});
});
- describe('with confidentiality indicator', () => {
+ describe('with internal note badge', () => {
it.each`
status | condition
${true} | ${'shows'}
${false} | ${'hides'}
- `('$condition icon indicator when isConfidential is $status', ({ status }) => {
- createComponent({ isConfidential: status });
- expect(findConfidentialIndicator().exists()).toBe(status);
+ `('$condition badge when isInternalNote is $status', ({ status }) => {
+ createComponent({ isInternalNote: status });
+ expect(findInternalNoteIndicator().exists()).toBe(status);
});
- it('shows confidential indicator tooltip for project context', () => {
- createComponent({ isConfidential: true, noteableType: 'issue' });
+ it('shows internal note badge tooltip for project context', () => {
+ createComponent({ isInternalNote: true, noteableType: 'issue' });
- expect(findConfidentialIndicator().attributes('title')).toBe(
+ expect(findInternalNoteIndicator().attributes('title')).toBe(
'This internal note will always remain confidential',
);
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index c46d3bbe5b2..ddfa77117ca 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -87,10 +87,27 @@ describe('noteable_discussion component', () => {
expect(noteFormProps.discussion).toBe(discussionMock);
expect(noteFormProps.line).toBe(null);
- expect(noteFormProps.saveButtonTitle).toBe('Comment');
expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`);
});
+ it.each`
+ noteType | isNoteInternal | saveButtonTitle
+ ${'public'} | ${false} | ${'Reply'}
+ ${'internal'} | ${true} | ${'Reply internally'}
+ `(
+ 'reply button on form should have title "$saveButtonTitle" when note is $noteType',
+ async ({ isNoteInternal, saveButtonTitle }) => {
+ wrapper.setProps({ discussion: { ...discussionMock, confidential: isNoteInternal } });
+ await nextTick();
+
+ const replyPlaceholder = wrapper.find(ReplyPlaceholder);
+ replyPlaceholder.vm.$emit('focus');
+ await nextTick();
+
+ expect(wrapper.find(NoteForm).props('saveButtonTitle')).toBe(saveButtonTitle);
+ },
+ );
+
it('should expand discussion', async () => {
const discussion = { ...discussionMock, expanded: false };
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 413ee815906..f4eb69e0d49 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -19,8 +19,6 @@ import '~/behaviors/markdown/render_gfm';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import * as mockData from '../mock_data';
-jest.mock('~/user_popovers', () => jest.fn());
-
setTestTimeout(1000);
const TYPE_COMMENT_FORM = 'comment-form';
@@ -224,7 +222,7 @@ describe('note_app', () => {
});
it('renders skeleton notes', () => {
- expect(wrapper.find('.animation-container').exists()).toBe(true);
+ expect(wrapper.find('.gl-skeleton-loader-default-container').exists()).toBe(true);
});
it('should render form', () => {
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index c7a6ca5eae3..9fa7166474a 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -785,7 +785,7 @@ export const notesWithDescriptionChanges = [
current_user: { can_edit: false, can_award_emoji: true },
resolved: false,
resolved_by: null,
- system_note_icon_name: 'pencil-square',
+ system_note_icon_name: 'pencil',
discussion_id: '7f1feda384083eb31763366e6392399fde6f3f31',
emoji_awardable: false,
report_abuse_path:
@@ -874,7 +874,7 @@ export const notesWithDescriptionChanges = [
current_user: { can_edit: false, can_award_emoji: true },
resolved: false,
resolved_by: null,
- system_note_icon_name: 'pencil-square',
+ system_note_icon_name: 'pencil',
discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
emoji_awardable: false,
report_abuse_path:
@@ -918,7 +918,7 @@ export const notesWithDescriptionChanges = [
current_user: { can_edit: false, can_award_emoji: true },
resolved: false,
resolved_by: null,
- system_note_icon_name: 'pencil-square',
+ system_note_icon_name: 'pencil',
discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
emoji_awardable: false,
report_abuse_path:
@@ -1105,7 +1105,7 @@ export const collapsedSystemNotes = [
current_user: { can_edit: false, can_award_emoji: true },
resolved: false,
resolved_by: null,
- system_note_icon_name: 'pencil-square',
+ system_note_icon_name: 'pencil',
discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
emoji_awardable: false,
report_abuse_path:
@@ -1149,7 +1149,7 @@ export const collapsedSystemNotes = [
current_user: { can_edit: false, can_award_emoji: true },
resolved: false,
resolved_by: null,
- system_note_icon_name: 'pencil-square',
+ system_note_icon_name: 'pencil',
discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
emoji_awardable: false,
report_abuse_path:
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index ecb213590ad..38f29ac2559 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -404,13 +404,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
afterEach(() => {
axiosMock.restore();
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', () => {
@@ -440,7 +440,7 @@ describe('Actions Notes Store', () => {
it('dispatches removeDiscussionsFromDiff on merge request page', () => {
const note = { path: endpoint, id: 1 };
- document.body.setAttribute('data-page', 'projects:merge_requests:show');
+ document.body.dataset.page = 'projects:merge_requests:show';
return testAction(
actions.removeNote,
@@ -473,13 +473,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
afterEach(() => {
axiosMock.restore();
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
it('dispatches removeNote', () => {
@@ -1382,6 +1382,29 @@ describe('Actions Notes Store', () => {
],
);
});
+
+ it('dispatches `fetchDiscussionsBatch` action if `paginatedMrDiscussions` feature flag is enabled', () => {
+ window.gon = { features: { paginatedMrDiscussions: true } };
+
+ return testAction(
+ actions.fetchDiscussions,
+ { path: 'test-path', filter: 'test-filter', persistFilter: 'test-persist-filter' },
+ null,
+ [],
+ [
+ {
+ type: 'fetchDiscussionsBatch',
+ payload: {
+ config: {
+ params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' },
+ },
+ path: 'test-path',
+ perPage: 20,
+ },
+ },
+ ],
+ );
+ });
});
describe('fetchDiscussionsBatch', () => {
@@ -1401,6 +1424,7 @@ describe('Actions Notes Store', () => {
null,
[
{ type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } },
+ { type: mutationTypes.SET_DONE_FETCHING_BATCH_DISCUSSIONS, payload: true },
{ type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
],
[{ type: 'updateResolvableDiscussionsCounts' }],
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index da1547ab6e7..e0a0fc43ffe 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -883,4 +883,16 @@ describe('Notes Store mutations', () => {
expect(state.discussions[0].position).toEqual(position);
});
});
+
+ describe('SET_DONE_FETCHING_BATCH_DISCUSSIONS', () => {
+ it('should set doneFetchingBatchDiscussions', () => {
+ const state = {
+ doneFetchingBatchDiscussions: false,
+ };
+
+ mutations.SET_DONE_FETCHING_BATCH_DISCUSSIONS(state, true);
+
+ expect(state.doneFetchingBatchDiscussions).toEqual(true);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index 057312828ff..84f01f10f21 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -10,6 +10,7 @@ import {
MISSING_MANIFEST_WARNING_TOOLTIP,
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
+ COPY_IMAGE_PATH_TITLE,
} from '~/packages_and_registries/container_registry/explorer/constants/index';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
@@ -150,7 +151,7 @@ describe('tags list row', () => {
expect(findClipboardButton().attributes()).toMatchObject({
text: tag.location,
- title: tag.location,
+ title: COPY_IMAGE_PATH_TITLE,
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index af5723267f4..0581a40b6a2 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
@@ -16,6 +16,7 @@ describe('cleanup_status', () => {
let wrapper;
const findMainIcon = () => wrapper.findByTestId('main-icon');
+ const findMainIconName = () => wrapper.findByTestId('main-icon').find(GlIcon);
const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
const findPopover = () => wrapper.findComponent(GlPopover);
@@ -61,6 +62,23 @@ describe('cleanup_status', () => {
expect(findMainIcon().exists()).toBe(true);
});
+
+ it.each`
+ status | visible | iconName
+ ${UNFINISHED_STATUS} | ${true} | ${'expire'}
+ ${SCHEDULED_STATUS} | ${true} | ${'clock'}
+ ${ONGOING_STATUS} | ${true} | ${'clock'}
+ ${UNSCHEDULED_STATUS} | ${false} | ${''}
+ `('matches "$iconName" when the status is "$status"', ({ status, visible, iconName }) => {
+ mountComponent({ status });
+
+ expect(findMainIcon().exists()).toBe(visible);
+ if (visible) {
+ const actualIcon = findMainIconName();
+ expect(actualIcon.exists()).toBe(true);
+ expect(actualIcon.props('name')).toBe(iconName);
+ }
+ });
});
describe('extra info icon', () => {
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 690d827ec67..979e1500d7d 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
@@ -13,6 +13,7 @@ import {
IMAGE_MIGRATING_STATE,
SCHEDULED_STATUS,
ROOT_IMAGE_TEXT,
+ COPY_IMAGE_PATH_TITLE,
} from '~/packages_and_registries/container_registry/explorer/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
@@ -106,7 +107,7 @@ describe('Image List Row', () => {
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(item.location);
- expect(button.props('title')).toBe(item.location);
+ expect(button.props('title')).toBe(COPY_IMAGE_PATH_TITLE);
});
describe('cleanup status component', () => {
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 f811468550d..a006de9f00c 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
@@ -93,7 +93,7 @@ describe('registry_header', () => {
expect(text.exists()).toBe(true);
expect(text.props()).toMatchObject({
text: EXPIRATION_POLICY_DISABLED_TEXT,
- icon: 'expire',
+ icon: 'clock',
size: 'xl',
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
index 6b6c33b7561..95de2f0bb0b 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
@@ -206,19 +206,19 @@ describe('Package Files', () => {
it('toggles the details row', async () => {
createComponent();
- expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down');
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-lg-down');
findFirstToggleDetailsButton().vm.$emit('click');
await nextTick();
expect(findFirstRowShaComponent('sha-256').exists()).toBe(true);
- expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-up');
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-lg-up');
findFirstToggleDetailsButton().vm.$emit('click');
await nextTick();
expect(findFirstRowShaComponent('sha-256').exists()).toBe(false);
- expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down');
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-lg-down');
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
index e5230417c78..a086c20a5e7 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
@@ -65,7 +65,7 @@ describe('Infrastructure Search', () => {
expect(findRegistrySearch().exists()).toBe(true);
expect(findRegistrySearch().props()).toMatchObject({
- filter: store.state.filter,
+ filters: store.state.filter,
sorting: store.state.sorting,
tokens: [],
sortableFields: sortableFields(),
@@ -80,7 +80,7 @@ describe('Infrastructure Search', () => {
mountComponent(isGroupPage);
expect(findRegistrySearch().props()).toMatchObject({
- filter: store.state.filter,
+ filters: store.state.filter,
sorting: store.state.sorting,
tokens: [],
sortableFields: fields,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
index 7a71a1cea0f..4f3d780b149 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
@@ -1,4 +1,9 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import {
conanMetadata,
mavenMetadata,
@@ -6,9 +11,11 @@ import {
packageData,
composerMetadata,
pypiMetadata,
+ packageMetadataQuery,
} from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import {
+ FETCH_PACKAGE_METADATA_ERROR_MESSAGE,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
@@ -16,6 +23,9 @@ import {
PACKAGE_TYPE_COMPOSER,
PACKAGE_TYPE_PYPI,
} from '~/packages_and_registries/package_registry/constants';
+import AdditionalMetadataLoader from '~/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import getPackageMetadata from '~/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql';
const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() };
const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() };
@@ -24,16 +34,26 @@ const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composer
const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() };
const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} };
-describe('Package Additional Metadata', () => {
+Vue.use(VueApollo);
+
+describe('Package Additional metadata', () => {
let wrapper;
+ let apolloProvider;
+
const defaultProps = {
- packageEntity: {
- ...packageData(mavenPackage),
- },
+ packageId: packageData().id,
+ packageType: PACKAGE_TYPE_MAVEN,
};
- const mountComponent = (props) => {
+ const mountComponent = ({
+ props = {},
+ resolver = jest.fn().mockResolvedValue(packageMetadataQuery(mavenPackage)),
+ } = {}) => {
+ const requestHandlers = [[getPackageMetadata, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMountExtended(component, {
+ apolloProvider,
propsData: { ...defaultProps, ...props },
stubs: {
component: { template: '<div data-testid="component-is"></div>' },
@@ -41,6 +61,10 @@ describe('Package Additional Metadata', () => {
});
};
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ });
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -49,6 +73,22 @@ describe('Package Additional Metadata', () => {
const findTitle = () => wrapper.findByTestId('title');
const findMainArea = () => wrapper.findByTestId('main');
const findComponentIs = () => wrapper.findByTestId('component-is');
+ const findAdditionalMetadataLoader = () => wrapper.findComponent(AdditionalMetadataLoader);
+ const findPackageMetadataAlert = () => wrapper.findComponent(GlAlert);
+
+ it('renders the loading container when loading', () => {
+ mountComponent();
+
+ expect(findAdditionalMetadataLoader().exists()).toBe(true);
+ });
+
+ it('does not render the loading container once resolved', async () => {
+ mountComponent();
+ await waitForPromises();
+
+ expect(findAdditionalMetadataLoader().exists()).toBe(false);
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
it('has the correct title', () => {
mountComponent();
@@ -56,7 +96,25 @@ describe('Package Additional Metadata', () => {
const title = findTitle();
expect(title.exists()).toBe(true);
- expect(title.text()).toBe('Additional Metadata');
+ expect(title.text()).toMatchInterpolatedText(component.i18n.componentTitle);
+ });
+
+ it('does not render gl-alert', () => {
+ mountComponent();
+
+ expect(findPackageMetadataAlert().exists()).toBe(false);
+ });
+
+ it('renders gl-alert if load fails', async () => {
+ mountComponent({ resolver: jest.fn().mockRejectedValue() });
+
+ await waitForPromises();
+
+ expect(findPackageMetadataAlert().exists()).toBe(true);
+ expect(findPackageMetadataAlert().text()).toMatchInterpolatedText(
+ FETCH_PACKAGE_METADATA_ERROR_MESSAGE,
+ );
+ expect(Sentry.captureException).toHaveBeenCalled();
});
it.each`
@@ -68,16 +126,22 @@ describe('Package Additional Metadata', () => {
${pypiPackage} | ${true} | ${PACKAGE_TYPE_PYPI}
${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM}
`(
- `It is $visible that the component is visible when the package is $packageType`,
- ({ packageEntity, visible }) => {
- mountComponent({ packageEntity });
+ `component visibility is $visible when the package is $packageType`,
+ async ({ packageEntity, visible, packageType }) => {
+ const resolved = packageMetadataQuery(packageType);
+ const resolver = jest.fn().mockResolvedValue(resolved);
+
+ mountComponent({ props: { packageType }, resolver });
+
+ await waitForPromises();
+ await nextTick();
expect(findTitle().exists()).toBe(visible);
expect(findMainArea().exists()).toBe(visible);
expect(findComponentIs().exists()).toBe(visible);
if (visible) {
- expect(findComponentIs().props('packageEntity')).toEqual(packageEntity);
+ expect(findComponentIs().props('packageMetadata')).toEqual(packageEntity.metadata);
}
},
);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
index e744680cb9a..bb6846d354f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
@@ -1,22 +1,16 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import {
- packageData,
- composerMetadata,
-} from 'jest/packages_and_registries/package_registry/mock_data';
+import { composerMetadata } from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue';
-import { PACKAGE_TYPE_COMPOSER } from '~/packages_and_registries/package_registry/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
-const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composerMetadata() };
-
describe('Composer Metadata', () => {
let wrapper;
const mountComponent = () => {
wrapper = shallowMountExtended(component, {
- propsData: { packageEntity: packageData(composerPackage) },
+ propsData: { packageMetadata: composerMetadata() },
stubs: {
DetailsRow,
GlSprintf,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
index 46593047f1f..e7e47401aa1 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
@@ -1,22 +1,16 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import {
- conanMetadata,
- packageData,
-} from 'jest/packages_and_registries/package_registry/mock_data';
+import { conanMetadata } from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue';
-import { PACKAGE_TYPE_CONAN } from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
-const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() };
-
describe('Conan Metadata', () => {
let wrapper;
const mountComponent = () => {
wrapper = shallowMountExtended(component, {
propsData: {
- packageEntity: packageData(conanPackage),
+ packageMetadata: conanMetadata(),
},
stubs: {
DetailsRow,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
index bc54cf1cb98..8680d983042 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
@@ -1,24 +1,16 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import {
- mavenMetadata,
- packageData,
-} from 'jest/packages_and_registries/package_registry/mock_data';
+import { mavenMetadata } from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/metadata/maven.vue';
-import { PACKAGE_TYPE_MAVEN } from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
-const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() };
-
describe('Maven Metadata', () => {
let wrapper;
const mountComponent = () => {
wrapper = shallowMountExtended(component, {
propsData: {
- packageEntity: {
- ...packageData(mavenPackage),
- },
+ packageMetadata: mavenMetadata(),
},
stubs: {
DetailsRow,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
index f759fe7a81c..af3692023f0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
@@ -1,25 +1,17 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import {
- nugetMetadata,
- packageData,
-} from 'jest/packages_and_registries/package_registry/mock_data';
+import { nugetMetadata } from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/metadata/nuget.vue';
-import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
describe('Nuget Metadata', () => {
- let nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
+ let nugetPackageMetadata = { ...nugetMetadata() };
let wrapper;
- const mountComponent = () => {
+ const mountComponent = (props) => {
wrapper = shallowMountExtended(component, {
- propsData: {
- packageEntity: {
- ...packageData(nugetPackage),
- },
- },
+ propsData: { ...props },
stubs: {
DetailsRow,
GlSprintf,
@@ -37,7 +29,7 @@ describe('Nuget Metadata', () => {
const findElementLink = (container) => container.findComponent(GlLink);
beforeEach(() => {
- mountComponent({ packageEntity: nugetPackage });
+ mountComponent({ packageMetadata: nugetPackageMetadata });
});
it.each`
@@ -49,14 +41,14 @@ describe('Nuget Metadata', () => {
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
- expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]);
+ expect(findElementLink(element).attributes('href')).toBe(nugetPackageMetadata[link]);
});
describe('without source', () => {
beforeAll(() => {
- nugetPackage = {
- packageType: PACKAGE_TYPE_NUGET,
- metadata: { iconUrl: 'iconUrl', licenseUrl: 'licenseUrl' },
+ nugetPackageMetadata = {
+ iconUrl: 'iconUrl',
+ licenseUrl: 'licenseUrl',
};
});
@@ -67,9 +59,9 @@ describe('Nuget Metadata', () => {
describe('without license', () => {
beforeAll(() => {
- nugetPackage = {
- packageType: PACKAGE_TYPE_NUGET,
- metadata: { iconUrl: 'iconUrl', projectUrl: 'projectUrl' },
+ nugetPackageMetadata = {
+ iconUrl: 'iconUrl',
+ projectUrl: 'projectUrl',
};
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
index c4481c3f20b..d7c6ea8379d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
@@ -1,22 +1,17 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { packageData, pypiMetadata } from 'jest/packages_and_registries/package_registry/mock_data';
+import { pypiMetadata } from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/metadata/pypi.vue';
-import { PACKAGE_TYPE_PYPI } from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
-const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() };
-
describe('Package Additional Metadata', () => {
let wrapper;
const mountComponent = () => {
wrapper = shallowMountExtended(component, {
propsData: {
- packageEntity: {
- ...packageData(pypiPackage),
- },
+ packageMetadata: pypiMetadata(),
},
stubs: {
DetailsRow,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index f8a4ba8f3bc..0447ead0830 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -34,7 +34,7 @@ describe('Package Files', () => {
},
stubs: {
...stubChildren(PackageFiles),
- GlTable: false,
+ GlTableLite: false,
},
});
};
@@ -219,19 +219,19 @@ describe('Package Files', () => {
it('toggles the details row', async () => {
createComponent();
- expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down');
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-down');
findFirstToggleDetailsButton().vm.$emit('click');
await nextTick();
expect(findFirstRowShaComponent('sha-256').exists()).toBe(true);
- expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-up');
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-up');
findFirstToggleDetailsButton().vm.$emit('click');
await nextTick();
expect(findFirstRowShaComponent('sha-256').exists()).toBe(false);
- expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down');
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-down');
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index 57b8be40a7c..f4e6d43812d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -1,17 +1,29 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { stubComponent } from 'helpers/stub_component';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
packageData,
packagePipelines,
+ packagePipelinesQuery,
} from 'jest/packages_and_registries/package_registry/mock_data';
import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants';
import component from '~/packages_and_registries/package_registry/components/details/package_history.vue';
+import PackageHistoryLoader from '~/packages_and_registries/package_registry/components/details/package_history_loader.vue';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import getPackagePipelines from '~/packages_and_registries/package_registry/graphql/queries/get_package_pipelines.query.graphql';
+
+Vue.use(VueApollo);
describe('Package History', () => {
let wrapper;
+ let apolloProvider;
+
const defaultProps = {
projectName: 'baz project',
packageEntity: { ...packageData() },
@@ -22,8 +34,15 @@ describe('Package History', () => {
const createPipelines = (amount) =>
[...Array(amount)].map((x, index) => packagePipelines({ id: index + 1 })[0]);
- const mountComponent = (props) => {
+ const mountComponent = ({
+ props = {},
+ resolver = jest.fn().mockResolvedValue(packagePipelinesQuery()),
+ } = {}) => {
+ const requestHandlers = [[getPackagePipelines, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMountExtended(component, {
+ apolloProvider,
propsData: { ...defaultProps, ...props },
stubs: {
HistoryItem: stubComponent(HistoryItem, {
@@ -34,18 +53,40 @@ describe('Package History', () => {
});
};
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ });
+
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
+ const findPackageHistoryLoader = () => wrapper.findComponent(PackageHistoryLoader);
const findHistoryElement = (testId) => wrapper.findByTestId(testId);
const findElementLink = (container) => container.findComponent(GlLink);
const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip);
+ const findPackageHistoryAlert = () => wrapper.findComponent(GlAlert);
const findTitle = () => wrapper.findByTestId('title');
const findTimeline = () => wrapper.findByTestId('timeline');
- it('has the correct title', () => {
+ it('renders the loading container when loading', () => {
+ mountComponent();
+
+ expect(findPackageHistoryLoader().exists()).toBe(true);
+ });
+
+ it('does not render the loading container once resolved', async () => {
+ mountComponent();
+ await waitForPromises();
+
+ expect(findPackageHistoryLoader().exists()).toBe(false);
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('has the correct title', async () => {
mountComponent();
+ await waitForPromises();
const title = findTitle();
@@ -53,8 +94,9 @@ describe('Package History', () => {
expect(title.text()).toBe('History');
});
- it('has a timeline container', () => {
+ it('has a timeline container', async () => {
mountComponent();
+ await waitForPromises();
const title = findTimeline();
@@ -64,6 +106,24 @@ describe('Package History', () => {
);
});
+ it('does not render gl-alert', () => {
+ mountComponent();
+
+ expect(findPackageHistoryAlert().exists()).toBe(false);
+ });
+
+ it('renders gl-alert if load fails', async () => {
+ mountComponent({ resolver: jest.fn().mockRejectedValue() });
+
+ await waitForPromises();
+
+ expect(findPackageHistoryAlert().exists()).toBe(true);
+ expect(findPackageHistoryAlert().text()).toEqual(
+ 'Something went wrong while fetching the package history.',
+ );
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+
describe.each`
name | amount | icon | text | timeAgoTooltip | link
${'created-on'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'clock'} | ${'@gitlab-org/package-15 version 1.0.0 was first created'} | ${packageData().createdAt} | ${null}
@@ -78,11 +138,21 @@ describe('Package History', () => {
({ name, icon, text, timeAgoTooltip, link, amount }) => {
let element;
- beforeEach(() => {
- const packageEntity = { ...packageData(), pipelines: { nodes: createPipelines(amount) } };
+ beforeEach(async () => {
+ const packageEntity = { ...packageData() };
+ const pipelinesResolver = jest
+ .fn()
+ .mockResolvedValue(packagePipelinesQuery(createPipelines(amount)));
+
mountComponent({
- packageEntity,
+ props: {
+ packageEntity,
+ },
+ resolver: pipelinesResolver,
});
+
+ await waitForPromises();
+
element = findHistoryElement(name);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index 3670cfca8ea..19505618ff7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -134,7 +134,7 @@ describe('Package Search', () => {
await nextTick();
- expect(findRegistrySearch().props('filter')).toEqual(['foo']);
+ expect(findRegistrySearch().props('filters')).toEqual(['foo']);
});
it('on filter:submit emits update event', async () => {
@@ -175,7 +175,7 @@ describe('Package Search', () => {
expect(getQueryParams).toHaveBeenCalled();
expect(findRegistrySearch().props()).toMatchObject({
- filter: defaultQueryParamsMock.filters,
+ filters: defaultQueryParamsMock.filters,
sorting: defaultQueryParamsMock.sorting,
});
});
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 0a4747fc9ec..d40feee582f 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -148,6 +148,8 @@ export const conanMetadata = () => ({
recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable',
});
+const conanMetadataQuery = () => ({ ...conanMetadata(), __typename: 'ConanMetadata' });
+
export const composerMetadata = () => ({
targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
composerJson: {
@@ -156,23 +158,45 @@ export const composerMetadata = () => ({
},
});
+const composerMetadataQuery = () => ({
+ ...composerMetadata(),
+ __typename: 'ComposerMetadata',
+});
+
export const pypiMetadata = () => ({
+ id: 'pypi-1',
requiredPython: '1.0.0',
});
+const pypiMetadataQuery = () => ({ ...pypiMetadata(), __typename: 'PypiMetadata' });
+
export const mavenMetadata = () => ({
+ id: 'maven-1',
appName: 'appName',
appGroup: 'appGroup',
appVersion: 'appVersion',
path: 'path',
});
+const mavenMetadataQuery = () => ({ ...mavenMetadata(), __typename: 'MavenMetadata' });
+
export const nugetMetadata = () => ({
+ id: 'nuget-1',
iconUrl: 'iconUrl',
licenseUrl: 'licenseUrl',
projectUrl: 'projectUrl',
});
+const nugetMetadataQuery = () => ({ ...nugetMetadata(), __typename: 'NugetMetadata' });
+
+const packageTypeMetadataQueryMapping = {
+ CONAN: conanMetadataQuery,
+ COMPOSER: composerMetadataQuery,
+ PYPI: pypiMetadataQuery,
+ MAVEN: mavenMetadataQuery,
+ NUGET: nugetMetadataQuery,
+};
+
export const pagination = (extend) => ({
endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ',
hasNextPage: true,
@@ -223,6 +247,19 @@ export const packageDetailsQuery = (extendPackage) => ({
},
});
+export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({
+ data: {
+ package: {
+ id: 'gid://gitlab/Packages::Package/111',
+ pipelines: {
+ nodes: pipelines,
+ __typename: 'PipelineConnection',
+ },
+ __typename: 'PackageDetailsType',
+ },
+ },
+});
+
export const emptyPackageDetailsQuery = () => ({
data: {
package: {
@@ -231,6 +268,21 @@ export const emptyPackageDetailsQuery = () => ({
},
});
+export const packageMetadataQuery = (packageType) => {
+ return {
+ data: {
+ package: {
+ id: 'gid://gitlab/Packages::Package/111',
+ packageType,
+ metadata: {
+ ...(packageTypeMetadataQueryMapping[packageType]?.() ?? {}),
+ },
+ __typename: 'PackageDetailsType',
+ },
+ },
+ };
+};
+
export const packageDestroyMutation = () => ({
data: {
destroyPackage: {
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 a7e31d42c9e..3cadb001c58 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
@@ -23,6 +23,10 @@ import {
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_MAVEN,
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_PYPI,
+ PACKAGE_TYPE_NPM,
} from '~/packages_and_registries/package_registry/constants';
import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
@@ -160,15 +164,38 @@ describe('PackagesApp', () => {
});
});
- it('renders additional metadata and has the right props', async () => {
- createComponent();
+ describe('additional metadata', () => {
+ it.each`
+ packageType | visible
+ ${PACKAGE_TYPE_MAVEN} | ${true}
+ ${PACKAGE_TYPE_CONAN} | ${true}
+ ${PACKAGE_TYPE_NUGET} | ${true}
+ ${PACKAGE_TYPE_COMPOSER} | ${true}
+ ${PACKAGE_TYPE_PYPI} | ${true}
+ ${PACKAGE_TYPE_NPM} | ${false}
+ `(
+ `It is $visible that the component is visible when the package is $packageType`,
+ async ({ packageType, visible }) => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageType,
+ }),
+ ),
+ });
- await waitForPromises();
+ await waitForPromises();
- expect(findAdditionalMetadata().exists()).toBe(true);
- expect(findAdditionalMetadata().props()).toMatchObject({
- packageEntity: expect.objectContaining(packageWithoutTypename),
- });
+ expect(findAdditionalMetadata().exists()).toBe(visible);
+
+ if (visible) {
+ expect(findAdditionalMetadata().props()).toMatchObject({
+ packageId: packageWithoutTypename.id,
+ packageType,
+ });
+ }
+ },
+ );
});
it('renders installation commands and has the right props', async () => {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
index 22754d31f93..e60989b0949 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
@@ -134,12 +134,6 @@ describe('DependencyProxySettings', () => {
mountComponent();
});
- it('has the help prop correctly set', () => {
- expect(findEnableProxyToggle().props()).toMatchObject({
- help: component.i18n.enabledProxyHelpText,
- });
- });
-
it('has help text with a link', () => {
expect(findEnableProxyToggle().text()).toContain(
'To see the image prefix and what is in the cache, visit the Dependency Proxy',
@@ -157,12 +151,6 @@ describe('DependencyProxySettings', () => {
});
});
- it('has the help prop set to empty', () => {
- expect(findEnableProxyToggle().props()).toMatchObject({
- help: '',
- });
- });
-
it('the help text is not visible', () => {
expect(findToggleHelpLink().exists()).toBe(false);
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
index 841a9bf8290..faa313118f3 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Settings Form Cadence matches snapshot 1`] = `
+exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`] = `
<expiration-dropdown-stub
class="gl-mr-7 gl-mb-0!"
data-testid="cadence-dropdown"
@@ -11,7 +11,7 @@ exports[`Settings Form Cadence matches snapshot 1`] = `
/>
`;
-exports[`Settings Form Enable matches snapshot 1`] = `
+exports[`Container Expiration Policy Settings Form Enable matches snapshot 1`] = `
<expiration-toggle-stub
class="gl-mb-0!"
data-testid="enable-toggle"
@@ -19,7 +19,7 @@ exports[`Settings Form Enable matches snapshot 1`] = `
/>
`;
-exports[`Settings Form Keep N matches snapshot 1`] = `
+exports[`Container Expiration Policy Settings Form Keep N matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="keep-n-dropdown"
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
@@ -29,7 +29,7 @@ exports[`Settings Form Keep N matches snapshot 1`] = `
/>
`;
-exports[`Settings Form Keep Regex matches snapshot 1`] = `
+exports[`Container Expiration Policy Settings Form Keep Regex matches snapshot 1`] = `
<expiration-input-stub
data-testid="keep-regex-input"
description="Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{linkEnd}"
@@ -41,7 +41,7 @@ exports[`Settings Form Keep Regex matches snapshot 1`] = `
/>
`;
-exports[`Settings Form OlderThan matches snapshot 1`] = `
+exports[`Container Expiration Policy Settings Form OlderThan matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="older-than-dropdown"
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
@@ -51,7 +51,7 @@ exports[`Settings Form OlderThan matches snapshot 1`] = `
/>
`;
-exports[`Settings Form Remove regex matches snapshot 1`] = `
+exports[`Container Expiration Policy Settings Form Remove regex matches snapshot 1`] = `
<expiration-input-stub
data-testid="remove-regex-input"
description="Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}"
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index 465e6dc73e2..ca44e77e694 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
-import component from '~/packages_and_registries/settings/project/components/settings_form.vue';
+import component from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
@@ -14,7 +14,7 @@ import expirationPolicyQuery from '~/packages_and_registries/settings/project/gr
import Tracking from '~/tracking';
import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data';
-describe('Settings Form', () => {
+describe('Container Expiration Policy Settings Form', () => {
let wrapper;
let fakeApollo;
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
new file mode 100644
index 00000000000..aa3506771fa
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -0,0 +1,167 @@
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import component from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
+import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
+import {
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ UNAVAILABLE_FEATURE_INTRO_TEXT,
+ UNAVAILABLE_USER_FEATURE_TEXT,
+} from '~/packages_and_registries/settings/project/constants';
+import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+
+import {
+ expirationPolicyPayload,
+ emptyExpirationPolicyPayload,
+ containerExpirationPolicyData,
+} from '../mock_data';
+
+describe('Container expiration policy project settings', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const defaultProvidedValues = {
+ projectPath: 'path',
+ isAdmin: false,
+ adminSettingsPath: 'settingsPath',
+ enableHistoricEntries: false,
+ helpPagePath: 'helpPagePath',
+ showCleanupPolicyLink: false,
+ };
+
+ const findFormComponent = () => wrapper.find(ContainerExpirationPolicyForm);
+ const findAlert = () => wrapper.find(GlAlert);
+ const findSettingsBlock = () => wrapper.find(SettingsBlock);
+
+ const mountComponent = (provide = defaultProvidedValues, config) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlSprintf,
+ SettingsBlock,
+ },
+ mocks: {
+ $toast: {
+ show: jest.fn(),
+ },
+ },
+ provide,
+ ...config,
+ });
+ };
+
+ const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[expirationPolicyQuery, resolver]];
+
+ fakeApollo = createMockApollo(requestHandlers);
+ mountComponent(provide, {
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('isEdited status', () => {
+ it.each`
+ description | apiResponse | workingCopy | result
+ ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
+ ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
+ ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
+ ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
+ ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
+ `('$description', async ({ apiResponse, workingCopy, result }) => {
+ mountComponentWithApollo({
+ provide: { ...defaultProvidedValues, enableHistoricEntries: true },
+ resolver: jest.fn().mockResolvedValue(apiResponse),
+ });
+ await waitForPromises();
+
+ findFormComponent().vm.$emit('input', workingCopy);
+
+ await waitForPromises();
+
+ expect(findFormComponent().props('isEdited')).toBe(result);
+ });
+ });
+
+ it('renders the setting form', async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
+ });
+ await waitForPromises();
+
+ expect(findFormComponent().exists()).toBe(true);
+ expect(findSettingsBlock().props('collapsible')).toBe(false);
+ });
+
+ describe('the form is disabled', () => {
+ it('the form is hidden', () => {
+ mountComponent();
+
+ expect(findFormComponent().exists()).toBe(false);
+ });
+
+ it('shows an alert', () => {
+ mountComponent();
+
+ const text = findAlert().text();
+ expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
+ expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT);
+ });
+
+ describe('an admin is visiting the page', () => {
+ it('shows the admin part of the alert message', () => {
+ mountComponent({ ...defaultProvidedValues, isAdmin: true });
+
+ const sprintf = findAlert().find(GlSprintf);
+ expect(sprintf.text()).toBe('administration settings');
+ expect(sprintf.find(GlLink).attributes('href')).toBe(
+ defaultProvidedValues.adminSettingsPath,
+ );
+ });
+ });
+ });
+
+ describe('fetchSettingsError', () => {
+ beforeEach(async () => {
+ mountComponentWithApollo({
+ resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ });
+ await waitForPromises();
+ });
+
+ it('the form is hidden', () => {
+ expect(findFormComponent().exists()).toBe(false);
+ });
+
+ it('shows an alert', () => {
+ expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
+ });
+ });
+
+ describe('empty API response', () => {
+ it.each`
+ enableHistoricEntries | isShown
+ ${true} | ${true}
+ ${false} | ${false}
+ `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
+ mountComponentWithApollo({
+ provide: {
+ ...defaultProvidedValues,
+ enableHistoricEntries,
+ },
+ resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
+ });
+ await waitForPromises();
+
+ expect(findFormComponent().exists()).toBe(isShown);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index 0a72f0269ee..337991dfae0 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -1,165 +1,19 @@
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue';
-import SettingsForm from '~/packages_and_registries/settings/project/components/settings_form.vue';
-import {
- FETCH_SETTINGS_ERROR_MESSAGE,
- UNAVAILABLE_FEATURE_INTRO_TEXT,
- UNAVAILABLE_USER_FEATURE_TEXT,
-} from '~/packages_and_registries/settings/project/constants';
-import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
-import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
-import {
- expirationPolicyPayload,
- emptyExpirationPolicyPayload,
- containerExpirationPolicyData,
-} from '../mock_data';
-
-describe('Registry Settings App', () => {
+describe('Registry Settings app', () => {
let wrapper;
- let fakeApollo;
-
- const defaultProvidedValues = {
- projectPath: 'path',
- isAdmin: false,
- adminSettingsPath: 'settingsPath',
- enableHistoricEntries: false,
- helpPagePath: 'helpPagePath',
- showCleanupPolicyLink: false,
- };
-
- const findSettingsComponent = () => wrapper.find(SettingsForm);
- const findAlert = () => wrapper.find(GlAlert);
-
- const mountComponent = (provide = defaultProvidedValues, config) => {
- wrapper = shallowMount(component, {
- stubs: {
- GlSprintf,
- SettingsBlock,
- },
- mocks: {
- $toast: {
- show: jest.fn(),
- },
- },
- provide,
- ...config,
- });
- };
-
- const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => {
- Vue.use(VueApollo);
-
- const requestHandlers = [[expirationPolicyQuery, resolver]];
-
- fakeApollo = createMockApollo(requestHandlers);
- mountComponent(provide, {
- apolloProvider: fakeApollo,
- });
- };
+ const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy);
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
- describe('isEdited status', () => {
- it.each`
- description | apiResponse | workingCopy | result
- ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
- ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
- ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
- ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
- ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
- `('$description', async ({ apiResponse, workingCopy, result }) => {
- mountComponentWithApollo({
- provide: { ...defaultProvidedValues, enableHistoricEntries: true },
- resolver: jest.fn().mockResolvedValue(apiResponse),
- });
- await waitForPromises();
-
- findSettingsComponent().vm.$emit('input', workingCopy);
-
- await waitForPromises();
-
- expect(findSettingsComponent().props('isEdited')).toBe(result);
- });
- });
-
- it('renders the setting form', async () => {
- mountComponentWithApollo({
- resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
- });
- await waitForPromises();
-
- expect(findSettingsComponent().exists()).toBe(true);
- });
-
- describe('the form is disabled', () => {
- it('the form is hidden', () => {
- mountComponent();
-
- expect(findSettingsComponent().exists()).toBe(false);
- });
-
- it('shows an alert', () => {
- mountComponent();
-
- const text = findAlert().text();
- expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT);
- expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT);
- });
-
- describe('an admin is visiting the page', () => {
- it('shows the admin part of the alert message', () => {
- mountComponent({ ...defaultProvidedValues, isAdmin: true });
-
- const sprintf = findAlert().find(GlSprintf);
- expect(sprintf.text()).toBe('administration settings');
- expect(sprintf.find(GlLink).attributes('href')).toBe(
- defaultProvidedValues.adminSettingsPath,
- );
- });
- });
- });
-
- describe('fetchSettingsError', () => {
- beforeEach(async () => {
- mountComponentWithApollo({
- resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')),
- });
- await waitForPromises();
- });
-
- it('the form is hidden', () => {
- expect(findSettingsComponent().exists()).toBe(false);
- });
-
- it('shows an alert', () => {
- expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE);
- });
- });
-
- describe('empty API response', () => {
- it.each`
- enableHistoricEntries | isShown
- ${true} | ${true}
- ${false} | ${false}
- `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => {
- mountComponentWithApollo({
- provide: {
- ...defaultProvidedValues,
- enableHistoricEntries,
- },
- resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()),
- });
- await waitForPromises();
+ it('renders container expiration policy component', () => {
+ wrapper = shallowMount(component);
- expect(findSettingsComponent().exists()).toBe(isShown);
- });
+ expect(findContainerExpirationPolicy().exists()).toBe(true);
});
});
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index 3dd6023140f..e6e89806ce0 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -30,11 +30,11 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
<svg
aria-hidden="true"
class="gl-icon s8"
- data-testid="angle-right-icon"
+ data-testid="chevron-lg-right-icon"
role="img"
>
<use
- href="#angle-right"
+ href="#chevron-lg-right"
/>
</svg>
</span>
diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
index bd492a5ae8f..db9f96bff39 100644
--- a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
@@ -100,7 +100,7 @@ describe('Persisted Search', () => {
await nextTick();
- expect(findRegistrySearch().props('filter')).toEqual(['foo']);
+ expect(findRegistrySearch().props('filters')).toEqual(['foo']);
});
it('on filter:submit emits update event', async () => {
@@ -138,7 +138,7 @@ describe('Persisted Search', () => {
expect(getQueryParams).toHaveBeenCalled();
expect(findRegistrySearch().props()).toMatchObject({
- filter: defaultQueryParamsMock.filters,
+ filters: defaultQueryParamsMock.filters,
sorting: defaultQueryParamsMock.sorting,
});
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index ae5404f2d13..d5b4b3c22d8 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -190,7 +190,7 @@ describe('Interval Pattern Input Component', () => {
findCustomInput().setValue(newValue);
- await nextTick;
+ await nextTick();
expect(findSelectedRadioKey()).toBe(customKey);
});
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 30d5f89d2f6..46f83ac89e5 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
@@ -53,11 +53,13 @@ const defaultProps = {
showVisibilityConfirmModal: false,
};
+const FEATURE_ACCESS_LEVEL_ANONYMOUS = 30;
+
describe('Settings Panel', () => {
let wrapper;
const mountComponent = (
- { currentSettings = {}, ...customProps } = {},
+ { currentSettings = {}, glFeatures = {}, ...customProps } = {},
mountFn = shallowMount,
) => {
const propsData = {
@@ -68,6 +70,12 @@ describe('Settings Panel', () => {
return mountFn(settingsPanel, {
propsData,
+ provide: {
+ glFeatures: {
+ packageRegistryAccessLevel: false,
+ ...glFeatures,
+ },
+ },
});
};
@@ -95,6 +103,10 @@ describe('Settings Panel', () => {
const findContainerRegistryAccessLevelInput = () =>
wrapper.find('[name="project[project_feature_attributes][container_registry_access_level]"]');
const findPackageSettings = () => wrapper.find({ ref: 'package-settings' });
+ const findPackageAccessLevel = () =>
+ wrapper.find('[data-testid="package-registry-access-level"]');
+ const findPackageAccessLevels = () =>
+ wrapper.find('[name="project[project_feature_attributes][package_registry_access_level]"]');
const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
const findPagesSettings = () => wrapper.find({ ref: 'pages-settings' });
const findPagesAccessLevels = () =>
@@ -521,6 +533,101 @@ describe('Settings Panel', () => {
settingsPanel.i18n.packagesLabel,
);
});
+
+ it('should hide the package access level settings', () => {
+ wrapper = mountComponent();
+
+ expect(findPackageAccessLevel().exists()).toBe(false);
+ });
+
+ describe('packageRegistryAccessLevel feature flag = true', () => {
+ it('should hide the packages settings', () => {
+ wrapper = mountComponent({
+ glFeatures: { packageRegistryAccessLevel: true },
+ packagesAvailable: true,
+ });
+
+ expect(findPackageSettings().exists()).toBe(false);
+ });
+
+ it('should hide the package access level settings with packagesAvailable = false', () => {
+ wrapper = mountComponent({ glFeatures: { packageRegistryAccessLevel: true } });
+
+ expect(findPackageAccessLevel().exists()).toBe(false);
+ });
+
+ it('renders the package access level settings with packagesAvailable = true', () => {
+ wrapper = mountComponent({
+ glFeatures: { packageRegistryAccessLevel: true },
+ packagesAvailable: true,
+ });
+
+ expect(findPackageAccessLevel().exists()).toBe(true);
+ });
+
+ it.each`
+ visibilityLevel | output
+ ${visibilityOptions.PRIVATE} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]}
+ ${visibilityOptions.INTERNAL} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]}
+ ${visibilityOptions.PUBLIC} | ${[[30, 'Everyone']]}
+ `(
+ 'renders correct options when visibilityLevel is $visibilityLevel',
+ async ({ visibilityLevel, output }) => {
+ wrapper = mountComponent({
+ glFeatures: { packageRegistryAccessLevel: true },
+ packagesAvailable: true,
+ currentSettings: {
+ visibilityLevel,
+ },
+ });
+
+ expect(findPackageAccessLevels().props('options')).toStrictEqual(output);
+ },
+ );
+
+ it.each`
+ initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption
+ ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE}
+ ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE}
+ `(
+ 'changes option from $initialPackageRegistryOption to $expectedPackageRegistryOption when visibilityLevel changed from $initialProjectVisibilityLevel to $newProjectVisibilityLevel',
+ async ({
+ initialProjectVisibilityLevel,
+ newProjectVisibilityLevel,
+ initialPackageRegistryOption,
+ expectedPackageRegistryOption,
+ }) => {
+ wrapper = mountComponent({
+ glFeatures: { packageRegistryAccessLevel: true },
+ packagesAvailable: true,
+ currentSettings: {
+ visibilityLevel: initialProjectVisibilityLevel,
+ packageRegistryAccessLevel: initialPackageRegistryOption,
+ },
+ });
+
+ await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel);
+
+ expect(findPackageAccessLevels().props('value')).toStrictEqual(
+ expectedPackageRegistryOption,
+ );
+ },
+ );
+ });
});
describe('Pages', () => {
diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js
index 5422481439e..627e004ce3e 100644
--- a/spec/frontend/performance_bar/components/add_request_spec.js
+++ b/spec/frontend/performance_bar/components/add_request_spec.js
@@ -1,12 +1,16 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { GlFormInput, GlButton } from '@gitlab/ui';
import AddRequest from '~/performance_bar/components/add_request.vue';
describe('add request form', () => {
let wrapper;
+ const findGlFormInput = () => wrapper.findComponent(GlFormInput);
+ const findGlButton = () => wrapper.findComponent(GlButton);
+
beforeEach(() => {
- wrapper = shallowMount(AddRequest);
+ wrapper = mount(AddRequest);
});
afterEach(() => {
@@ -14,35 +18,35 @@ describe('add request form', () => {
});
it('hides the input on load', () => {
- expect(wrapper.find('input').exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
});
describe('when clicking the button', () => {
beforeEach(async () => {
- wrapper.find('button').trigger('click');
+ findGlButton().trigger('click');
await nextTick();
});
it('shows the form', () => {
- expect(wrapper.find('input').exists()).toBe(true);
+ expect(findGlFormInput().exists()).toBe(true);
});
describe('when pressing escape', () => {
beforeEach(async () => {
- wrapper.find('input').trigger('keyup.esc');
+ findGlFormInput().trigger('keyup.esc');
await nextTick();
});
it('hides the input', () => {
- expect(wrapper.find('input').exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
});
});
describe('when submitting the form', () => {
beforeEach(async () => {
- wrapper.find('input').setValue('http://gitlab.example.com/users/root/calendar.json');
+ findGlFormInput().setValue('http://gitlab.example.com/users/root/calendar.json');
await nextTick();
- wrapper.find('input').trigger('keyup.enter');
+ findGlFormInput().trigger('keyup.enter');
await nextTick();
});
@@ -54,13 +58,13 @@ describe('add request form', () => {
});
it('hides the input', () => {
- expect(wrapper.find('input').exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
});
it('clears the value for next time', async () => {
- wrapper.find('button').trigger('click');
+ findGlButton().trigger('click');
await nextTick();
- expect(wrapper.find('input').text()).toEqual('');
+ expect(findGlFormInput().text()).toEqual('');
});
});
});
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 6c1cbfa70a1..2da176dbfe4 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -17,11 +17,11 @@ describe('performance bar wrapper', () => {
performance.getEntriesByType = jest.fn().mockReturnValue([]);
peekWrapper.setAttribute('id', 'js-peek');
- peekWrapper.setAttribute('data-env', 'development');
- peekWrapper.setAttribute('data-request-id', '123');
- peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
- peekWrapper.setAttribute('data-stats-url', 'https://log.gprd.gitlab.net/app/dashboards#/view/');
- peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
+ peekWrapper.dataset.env = 'development';
+ peekWrapper.dataset.requestId = '123';
+ peekWrapper.dataset.peekUrl = '/-/peek/results';
+ peekWrapper.dataset.statsUrl = 'https://log.gprd.gitlab.net/app/dashboards#/view/';
+ peekWrapper.dataset.profileUrl = '?lineprofiler=true';
mock = new MockAdapter(axios);
@@ -69,7 +69,7 @@ describe('performance bar wrapper', () => {
it('adds the request immediately', () => {
vm.addRequest('123', 'https://gitlab.com/');
- expect(vm.store.addRequest).toHaveBeenCalledWith('123', 'https://gitlab.com/');
+ expect(vm.store.addRequest).toHaveBeenCalledWith('123', 'https://gitlab.com/', undefined);
});
});
diff --git a/spec/frontend/performance_bar/services/performance_bar_service_spec.js b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
index 36bfd575c12..1bb70a43a1b 100644
--- a/spec/frontend/performance_bar/services/performance_bar_service_spec.js
+++ b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
@@ -63,5 +63,17 @@ describe('PerformanceBarService', () => {
);
});
});
+
+ describe('operationName', () => {
+ function requestUrl(response, peekUrl) {
+ return PerformanceBarService.callbackParams(response, peekUrl)[3];
+ }
+
+ it('gets the operation name from response.config', () => {
+ expect(
+ requestUrl({ headers: {}, config: { operationName: 'someOperation' } }, '/peek'),
+ ).toBe('someOperation');
+ });
+ });
});
});
diff --git a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
index b7324ba2f6e..7d5c5031792 100644
--- a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
+++ b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
@@ -1,9 +1,9 @@
import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
describe('PerformanceBarStore', () => {
- describe('truncateUrl', () => {
+ describe('displayName', () => {
let store;
- const findUrl = (id) => store.findRequest(id).truncatedUrl;
+ const findUrl = (id) => store.findRequest(id).displayName;
beforeEach(() => {
store = new PerformanceBarStore();
@@ -41,6 +41,11 @@ describe('PerformanceBarStore', () => {
store.addRequest('id', 'http://localhost:3001/h5bp/html5-boilerplate/#frag/ment');
expect(findUrl('id')).toEqual('html5-boilerplate');
});
+
+ it('appends the GraphQL operation name', () => {
+ store.addRequest('id', 'http://localhost:3001/api/graphql', 'someOperation');
+ expect(findUrl('id')).toBe('graphql (someOperation)');
+ });
});
describe('setRequestDetailsData', () => {
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index e435c0dcc08..bf5d15516c2 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -1,9 +1,12 @@
import { getByRole } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
+import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
describe('First pipeline card', () => {
let wrapper;
+ let trackingSpy;
const defaultProvide = {
runnerHelpPagePath: '/help/runners',
@@ -17,7 +20,7 @@ describe('First pipeline card', () => {
});
};
- const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href;
+ const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name });
const findRunnersLink = () => getLinkByName(/make sure your instance has runners available/i);
const findInstructionsList = () => wrapper.find('ol');
const findAllInstructions = () => findInstructionsList().findAll('li');
@@ -40,6 +43,26 @@ describe('First pipeline card', () => {
});
it('renders the link', () => {
- expect(findRunnersLink()).toContain(defaultProvide.runnerHelpPagePath);
+ expect(findRunnersLink().href).toContain(defaultProvide.runnerHelpPagePath);
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks runners help page click', async () => {
+ const { label } = pipelineEditorTrackingOptions;
+ const { runners } = pipelineEditorTrackingOptions.actions.helpDrawerLinks;
+
+ await findRunnersLink().click();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, runners, { label });
+ });
});
});
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
index 3c8821d05a7..49177befe0e 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -1,9 +1,12 @@
import { getByRole } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
+import { pipelineEditorTrackingOptions } from '~/pipeline_editor/constants';
describe('Pipeline config reference card', () => {
let wrapper;
+ let trackingSpy;
const defaultProvide = {
ciExamplesHelpPagePath: 'help/ci/examples/',
@@ -20,7 +23,7 @@ describe('Pipeline config reference card', () => {
});
};
- const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href;
+ const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name });
const findCiExamplesLink = () => getLinkByName(/CI\/CD examples and templates/i);
const findCiIntroLink = () => getLinkByName(/GitLab CI\/CD concepts/i);
const findNeedsLink = () => getLinkByName(/Needs keyword/i);
@@ -43,9 +46,44 @@ describe('Pipeline config reference card', () => {
});
it('renders the links', () => {
- expect(findCiExamplesLink()).toContain(defaultProvide.ciExamplesHelpPagePath);
- expect(findCiIntroLink()).toContain(defaultProvide.ciHelpPagePath);
- expect(findNeedsLink()).toContain(defaultProvide.needsHelpPagePath);
- expect(findYmlSyntaxLink()).toContain(defaultProvide.ymlHelpPagePath);
+ expect(findCiExamplesLink().href).toContain(defaultProvide.ciExamplesHelpPagePath);
+ expect(findCiIntroLink().href).toContain(defaultProvide.ciHelpPagePath);
+ expect(findNeedsLink().href).toContain(defaultProvide.needsHelpPagePath);
+ expect(findYmlSyntaxLink().href).toContain(defaultProvide.ymlHelpPagePath);
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ const testTracker = async (element, expectedAction) => {
+ const { label } = pipelineEditorTrackingOptions;
+
+ await element.click();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, expectedAction, {
+ label,
+ });
+ };
+
+ it('tracks help page links', async () => {
+ const {
+ CI_EXAMPLES_LINK,
+ CI_HELP_LINK,
+ CI_NEEDS_LINK,
+ CI_YAML_LINK,
+ } = pipelineEditorTrackingOptions.actions.helpDrawerLinks;
+
+ testTracker(findCiExamplesLink(), CI_EXAMPLES_LINK);
+ testTracker(findCiIntroLink(), CI_HELP_LINK);
+ testTracker(findNeedsLink(), CI_NEEDS_LINK);
+ testTracker(findYmlSyntaxLink(), CI_YAML_LINK);
+ });
});
});
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
index 8f50325295e..930f08ef545 100644
--- a/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -29,6 +29,17 @@ describe('CI Editor Header', () => {
unmockTracking();
});
+ const testTracker = async (element, expectedAction) => {
+ const { label } = pipelineEditorTrackingOptions;
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ await element.vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, expectedAction, {
+ label,
+ });
+ };
+
describe('link button', () => {
beforeEach(() => {
createComponent();
@@ -48,13 +59,9 @@ describe('CI Editor Header', () => {
});
it('tracks the click on the browse button', async () => {
- const { label, actions } = pipelineEditorTrackingOptions;
-
- await findLinkBtn().vm.$emit('click');
+ const { browseTemplates } = pipelineEditorTrackingOptions.actions;
- expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.browse_templates, {
- label,
- });
+ testTracker(findLinkBtn(), browseTemplates);
});
});
@@ -72,21 +79,31 @@ describe('CI Editor Header', () => {
});
describe('when pipeline editor drawer is closed', () => {
- it('emits open drawer event when clicked', () => {
+ beforeEach(() => {
createComponent({ showDrawer: false });
+ });
+ it('emits open drawer event when clicked', () => {
expect(wrapper.emitted('open-drawer')).toBeUndefined();
findHelpBtn().vm.$emit('click');
expect(wrapper.emitted('open-drawer')).toHaveLength(1);
});
+
+ it('tracks open help drawer action', async () => {
+ const { actions } = pipelineEditorTrackingOptions;
+
+ testTracker(findHelpBtn(), actions.openHelpDrawer);
+ });
});
describe('when pipeline editor drawer is open', () => {
- it('emits close drawer event when clicked', () => {
+ beforeEach(() => {
createComponent({ showDrawer: true });
+ });
+ it('emits close drawer event when clicked', () => {
expect(wrapper.emitted('close-drawer')).toBeUndefined();
findHelpBtn().vm.$emit('click');
diff --git a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
index a61796dbed2..d503aff40b8 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
@@ -23,7 +23,6 @@ describe('Pipeline editor file nav', () => {
const createComponent = ({
appStatus = EDITOR_APP_STATUS_VALID,
isNewCiConfigFile = false,
- pipelineEditorFileTree = false,
} = {}) => {
mockApollo.clients.defaultClient.cache.writeQuery({
query: getAppStatus,
@@ -38,11 +37,6 @@ describe('Pipeline editor file nav', () => {
wrapper = extendedWrapper(
shallowMount(PipelineEditorFileNav, {
apolloProvider: mockApollo,
- provide: {
- glFeatures: {
- pipelineEditorFileTree,
- },
- },
propsData: {
isNewCiConfigFile,
},
@@ -66,24 +60,12 @@ describe('Pipeline editor file nav', () => {
it('renders the branch switcher', () => {
expect(findBranchSwitcher().exists()).toBe(true);
});
-
- it('does not render the file tree button', () => {
- expect(findFileTreeBtn().exists()).toBe(false);
- });
-
- it('does not render the file tree popover', () => {
- expect(findPopoverContainer().exists()).toBe(false);
- });
});
- describe('with pipelineEditorFileTree feature flag ON', () => {
+ describe('file tree', () => {
describe('when editor is in the empty state', () => {
beforeEach(() => {
- createComponent({
- appStatus: EDITOR_APP_STATUS_EMPTY,
- isNewCiConfigFile: false,
- pipelineEditorFileTree: true,
- });
+ createComponent({ appStatus: EDITOR_APP_STATUS_EMPTY, isNewCiConfigFile: false });
});
it('does not render the file tree button', () => {
@@ -97,11 +79,7 @@ describe('Pipeline editor file nav', () => {
describe('when user is about to create their config file for the first time', () => {
beforeEach(() => {
- createComponent({
- appStatus: EDITOR_APP_STATUS_VALID,
- isNewCiConfigFile: true,
- pipelineEditorFileTree: true,
- });
+ createComponent({ appStatus: EDITOR_APP_STATUS_VALID, isNewCiConfigFile: true });
});
it('does not render the file tree button', () => {
@@ -115,11 +93,7 @@ describe('Pipeline editor file nav', () => {
describe('when app is in a global loading state', () => {
it('renders the file tree button with a loading icon', () => {
- createComponent({
- appStatus: EDITOR_APP_STATUS_LOADING,
- isNewCiConfigFile: false,
- pipelineEditorFileTree: true,
- });
+ createComponent({ appStatus: EDITOR_APP_STATUS_LOADING, isNewCiConfigFile: false });
expect(findFileTreeBtn().exists()).toBe(true);
expect(findFileTreeBtn().attributes('loading')).toBe('true');
@@ -128,11 +102,7 @@ describe('Pipeline editor file nav', () => {
describe('when editor has a non-empty config file open', () => {
beforeEach(() => {
- createComponent({
- appStatus: EDITOR_APP_STATUS_VALID,
- isNewCiConfigFile: false,
- pipelineEditorFileTree: true,
- });
+ createComponent({ appStatus: EDITOR_APP_STATUS_VALID, isNewCiConfigFile: false });
});
it('renders the file tree button', () => {
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 d159a20a8d6..3ecf6472544 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -3,8 +3,9 @@ import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
-import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
+import CiValidate from '~/pipeline_editor/components/validate/ci_validate.vue';
+import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
import {
@@ -13,9 +14,7 @@ import {
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_VALID,
- MERGED_TAB,
TAB_QUERY_PARAM,
- TABS_INDEX,
} from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
@@ -60,10 +59,12 @@ describe('Pipeline editor tabs component', () => {
const findEditorTab = () => wrapper.find('[data-testid="editor-tab"]');
const findLintTab = () => wrapper.find('[data-testid="lint-tab"]');
const findMergedTab = () => wrapper.find('[data-testid="merged-tab"]');
+ const findValidateTab = () => wrapper.find('[data-testid="validate-tab"]');
const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
const findAlert = () => wrapper.findComponent(GlAlert);
const findCiLint = () => wrapper.findComponent(CiLint);
+ const findCiValidate = () => wrapper.findComponent(CiValidate);
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
@@ -111,6 +112,61 @@ describe('Pipeline editor tabs component', () => {
});
});
+ describe('validate tab', () => {
+ describe('with simulatePipeline feature flag ON', () => {
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent({
+ appStatus: EDITOR_APP_STATUS_LOADING,
+ provide: {
+ glFeatures: {
+ simulatePipeline: true,
+ },
+ },
+ });
+ });
+
+ it('displays a loading icon if the lint query is loading', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not display the validate component', () => {
+ expect(findCiValidate().exists()).toBe(false);
+ });
+ });
+
+ describe('after loading', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: { glFeatures: { simulatePipeline: true } },
+ });
+ });
+
+ it('displays the tab and the validate component', () => {
+ expect(findValidateTab().exists()).toBe(true);
+ expect(findCiValidate().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('with simulatePipeline feature flag OFF', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ simulatePipeline: false,
+ },
+ },
+ });
+ });
+
+ it('does not render the tab and the validate component', () => {
+ expect(findValidateTab().exists()).toBe(false);
+ expect(findCiValidate().exists()).toBe(false);
+ });
+ });
+ });
+
describe('lint tab', () => {
describe('while loading', () => {
beforeEach(() => {
@@ -125,6 +181,7 @@ describe('Pipeline editor tabs component', () => {
expect(findCiLint().exists()).toBe(false);
});
});
+
describe('after loading', () => {
beforeEach(() => {
createComponent();
@@ -135,8 +192,24 @@ describe('Pipeline editor tabs component', () => {
expect(findCiLint().exists()).toBe(true);
});
});
- });
+ describe('with simulatePipeline feature flag ON', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ simulatePipeline: true,
+ },
+ },
+ });
+ });
+
+ it('does not render the tab and the lint component', () => {
+ expect(findLintTab().exists()).toBe(false);
+ expect(findCiLint().exists()).toBe(false);
+ });
+ });
+ });
describe('merged tab', () => {
describe('while loading', () => {
beforeEach(() => {
@@ -221,18 +294,6 @@ describe('Pipeline editor tabs component', () => {
search: `?${TAB_QUERY_PARAM}=${queryValue}`,
});
});
-
- it('is the tab specified in query param and transform it into an index value', async () => {
- setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${MERGED_TAB}`);
- createComponent();
-
- // If the query param has changed to an index, it means we have synced the
- // query with.
- expect(window.location).toMatchObject({
- ...matchObject,
- search: `?${TAB_QUERY_PARAM}=${TABS_INDEX[MERGED_TAB]}`,
- });
- });
});
describe('glTabs', () => {
diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
new file mode 100644
index 00000000000..25972317593
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
@@ -0,0 +1,40 @@
+import { GlButton, GlDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_validate.vue';
+
+describe('Pipeline Editor Validate Tab', () => {
+ let wrapper;
+
+ const createComponent = ({ stubs } = {}) => {
+ wrapper = shallowMount(CiValidate, {
+ provide: {
+ validateTabIllustrationPath: '/path/to/img',
+ },
+ stubs,
+ });
+ };
+
+ const findCta = () => wrapper.findComponent(GlButton);
+ const findPipelineSource = () => wrapper.findComponent(GlDropdown);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders disabled pipeline source dropdown', () => {
+ expect(findPipelineSource().exists()).toBe(true);
+ expect(findPipelineSource().attributes('text')).toBe(i18n.pipelineSourceDefault);
+ expect(findPipelineSource().attributes('disabled')).toBe('true');
+ });
+
+ it('renders CTA', () => {
+ expect(findCta().exists()).toBe(true);
+ expect(findCta().text()).toBe(i18n.cta);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index bf0f7fd8c9f..c6964f190b4 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { GlButton, GlDrawer, GlModal } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
import CiEditorHeader from '~/pipeline_editor/components/editor/ci_editor_header.vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
@@ -11,11 +12,12 @@ import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switche
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import {
- MERGED_TAB,
- VISUALIZE_TAB,
CREATE_TAB,
- LINT_TAB,
FILE_TREE_DISPLAY_KEY,
+ LINT_TAB,
+ MERGED_TAB,
+ TABS_INDEX,
+ VISUALIZE_TAB,
} from '~/pipeline_editor/constants';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
@@ -162,6 +164,24 @@ describe('Pipeline editor home wrapper', () => {
await nextTick();
expect(findCommitSection().exists()).toBe(true);
});
+
+ describe('rendering with tab params', () => {
+ it.each`
+ tab | shouldShow
+ ${MERGED_TAB} | ${false}
+ ${VISUALIZE_TAB} | ${false}
+ ${LINT_TAB} | ${false}
+ ${CREATE_TAB} | ${true}
+ `(
+ 'when the tab query param is $tab the commit form is shown: $shouldShow',
+ async ({ tab, shouldShow }) => {
+ setWindowLocation(`https://gitlab.test/ci/editor/?tab=${TABS_INDEX[tab]}`);
+ await createComponent({ stubs: { PipelineEditorTabs } });
+
+ expect(findCommitSection().exists()).toBe(shouldShow);
+ },
+ );
+ });
});
describe('WalkthroughPopover events', () => {
@@ -247,81 +267,63 @@ describe('Pipeline editor home wrapper', () => {
await nextTick();
};
- describe('with pipelineEditorFileTree feature flag OFF', () => {
+ describe('button toggle', () => {
beforeEach(() => {
- createComponent();
+ createComponent({
+ stubs: {
+ GlButton,
+ PipelineEditorFileNav,
+ },
+ });
});
- it('hides the file tree', () => {
- expect(findFileTreeBtn().exists()).toBe(false);
- expect(findPipelineEditorFileTree().exists()).toBe(false);
+ it('shows button toggle', () => {
+ expect(findFileTreeBtn().exists()).toBe(true);
});
- });
-
- describe('with pipelineEditorFileTree feature flag ON', () => {
- describe('button toggle', () => {
- beforeEach(() => {
- createComponent({
- glFeatures: {
- pipelineEditorFileTree: true,
- },
- stubs: {
- GlButton,
- PipelineEditorFileNav,
- },
- });
- });
-
- it('shows button toggle', () => {
- expect(findFileTreeBtn().exists()).toBe(true);
- });
- it('toggles the drawer on button click', async () => {
- await toggleFileTree();
+ it('toggles the drawer on button click', async () => {
+ await toggleFileTree();
- expect(findPipelineEditorFileTree().exists()).toBe(true);
+ expect(findPipelineEditorFileTree().exists()).toBe(true);
- await toggleFileTree();
+ await toggleFileTree();
- expect(findPipelineEditorFileTree().exists()).toBe(false);
- });
+ expect(findPipelineEditorFileTree().exists()).toBe(false);
+ });
- it('sets the display state in local storage', async () => {
- await toggleFileTree();
+ it('sets the display state in local storage', async () => {
+ await toggleFileTree();
- expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('true');
+ expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('true');
- await toggleFileTree();
+ await toggleFileTree();
- expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('false');
- });
+ expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('false');
});
+ });
- describe('when file tree display state is saved in local storage', () => {
- beforeEach(() => {
- localStorage.setItem(FILE_TREE_DISPLAY_KEY, 'true');
- createComponent({
- glFeatures: { pipelineEditorFileTree: true },
- stubs: { PipelineEditorFileNav },
- });
+ describe('when file tree display state is saved in local storage', () => {
+ beforeEach(() => {
+ localStorage.setItem(FILE_TREE_DISPLAY_KEY, 'true');
+ createComponent({
+ stubs: { PipelineEditorFileNav },
});
+ });
- it('shows the file tree by default', () => {
- expect(findPipelineEditorFileTree().exists()).toBe(true);
- });
+ it('shows the file tree by default', () => {
+ expect(findPipelineEditorFileTree().exists()).toBe(true);
});
+ });
- describe('when file tree display state is not saved in local storage', () => {
- beforeEach(() => {
- createComponent({
- glFeatures: { pipelineEditorFileTree: true },
- stubs: { PipelineEditorFileNav },
- });
+ describe('when file tree display state is not saved in local storage', () => {
+ beforeEach(() => {
+ createComponent({
+ stubs: { PipelineEditorFileNav },
});
+ });
- it('hides the file tree by default', () => {
- expect(findPipelineEditorFileTree().exists()).toBe(false);
- });
+ it('hides the file tree by default', () => {
+ expect(findPipelineEditorFileTree().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipeline_wizard/components/input_spec.js b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
index ee1f3fe70ff..ea2448b1362 100644
--- a/spec/frontend/pipeline_wizard/components/input_spec.js
+++ b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
@@ -1,6 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
import { Document } from 'yaml';
-import InputWrapper from '~/pipeline_wizard/components/input.vue';
+import InputWrapper from '~/pipeline_wizard/components/input_wrapper.vue';
import TextWidget from '~/pipeline_wizard/components/widgets/text.vue';
describe('Pipeline Wizard -- Input Wrapper', () => {
diff --git a/spec/frontend/pipeline_wizard/components/step_spec.js b/spec/frontend/pipeline_wizard/components/step_spec.js
index 2289a349318..aa87b1d0b04 100644
--- a/spec/frontend/pipeline_wizard/components/step_spec.js
+++ b/spec/frontend/pipeline_wizard/components/step_spec.js
@@ -3,7 +3,7 @@ import { omit } from 'lodash';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineWizardStep from '~/pipeline_wizard/components/step.vue';
-import InputWrapper from '~/pipeline_wizard/components/input.vue';
+import InputWrapper from '~/pipeline_wizard/components/input_wrapper.vue';
import StepNav from '~/pipeline_wizard/components/step_nav.vue';
import {
stepInputs,
diff --git a/spec/frontend/pipeline_wizard/components/widgets_spec.js b/spec/frontend/pipeline_wizard/components/widgets_spec.js
index 5944c76c5d0..6bd858e746c 100644
--- a/spec/frontend/pipeline_wizard/components/widgets_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets_spec.js
@@ -1,7 +1,7 @@
import fs from 'fs';
import { mount } from '@vue/test-utils';
import { Document } from 'yaml';
-import InputWrapper from '~/pipeline_wizard/components/input.vue';
+import InputWrapper from '~/pipeline_wizard/components/input_wrapper.vue';
describe('Test all widgets in ./widgets/* whether they provide a minimal api', () => {
const createComponent = (props = {}, mountFunc = mount) => {
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index 89002ee47a8..e0210307823 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -1,4 +1,5 @@
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';
@@ -21,35 +22,35 @@ describe('The Pipeline Tabs', () => {
const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
const findTestsApp = () => wrapper.findComponent(TestReports);
+ const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter');
+ const findJobsBadge = () => wrapper.findByTestId('builds-counter');
+
const defaultProvide = {
defaultTabValue: '',
+ failedJobsCount: 1,
+ failedJobsSummary: [],
+ totalJobCount: 10,
};
- const createComponent = (propsData = {}) => {
+ const createComponent = (provide = {}) => {
wrapper = extendedWrapper(
shallowMount(PipelineTabs, {
- propsData,
provide: {
...defaultProvide,
+ ...provide,
},
stubs: {
- JobsApp: { template: '<div class="jobs" />' },
+ GlTab,
TestReports: { template: '<div id="tests" />' },
},
}),
);
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- // The failed jobs MUST be removed from here and tested individually once
- // the logic for the tab is implemented.
describe('Tabs', () => {
it.each`
tabName | tabComponent | appComponent
@@ -58,9 +59,34 @@ describe('The Pipeline Tabs', () => {
${'Jobs'} | ${findJobsTab} | ${findJobsApp}
${'Failed Jobs'} | ${findFailedJobsTab} | ${findFailedJobsApp}
${'Tests'} | ${findTestsTab} | ${findTestsApp}
- `('shows $tabName tab and its associated component', ({ appComponent, tabComponent }) => {
+ `('shows $tabName tab with its associated component', ({ appComponent, tabComponent }) => {
+ createComponent();
+
expect(tabComponent().exists()).toBe(true);
expect(appComponent().exists()).toBe(true);
});
+
+ describe('with no failed jobs', () => {
+ beforeEach(() => {
+ createComponent({ failedJobsCount: 0 });
+ });
+
+ it('hides the failed jobs tab', () => {
+ expect(findFailedJobsTab().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Tabs badges', () => {
+ it.each`
+ tabName | badgeComponent | badgeText
+ ${'Jobs'} | ${findJobsBadge} | ${String(defaultProvide.totalJobCount)}
+ ${'Failed Jobs'} | ${findFailedJobsBadge} | ${String(defaultProvide.failedJobsCount)}
+ `('shows badge for $tabName with the correct text', ({ badgeComponent, badgeText }) => {
+ createComponent();
+
+ expect(badgeComponent().exists()).toBe(true);
+ expect(badgeComponent().text()).toBe(badgeText);
+ });
});
});
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
index 6d0e99ff63e..1ff32b03344 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js
@@ -5,6 +5,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
+import waitForPromises from 'helpers/wait_for_promises';
import { stageReply } from '../../mock_data';
const dropdownPath = 'path.json';
@@ -55,7 +56,10 @@ describe('Pipelines stage component', () => {
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
+ const findDropdownMenuTitle = () =>
+ wrapper.find('[data-testid="pipeline-stage-dropdown-menu-title"]');
const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]');
+ const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]');
const openStageDropdown = () => {
findDropdownToggle().trigger('click');
@@ -64,6 +68,27 @@ describe('Pipelines stage component', () => {
});
};
+ describe('loading state', () => {
+ beforeEach(async () => {
+ createComponent({ updateDropdown: true });
+
+ mock.onGet(dropdownPath).reply(200, stageReply);
+
+ await openStageDropdown();
+ });
+
+ it('displays loading state while jobs are being fetched', () => {
+ expect(findLoadingState().exists()).toBe(true);
+ expect(findLoadingState().text()).toBe(PipelineStage.i18n.loadingText);
+ });
+
+ it('does not display loading state after jobs have been fetched', async () => {
+ await waitForPromises();
+
+ expect(findLoadingState().exists()).toBe(false);
+ });
+ });
+
describe('default appearance', () => {
beforeEach(() => {
createComponent();
@@ -78,6 +103,17 @@ describe('Pipelines stage component', () => {
expect(findDropdownToggle().exists()).toBe(true);
expect(findCiIcon().exists()).toBe(true);
});
+
+ it('should render a borderless ci-icon', () => {
+ expect(findCiIcon().exists()).toBe(true);
+ expect(findCiIcon().props('isBorderless')).toBe(true);
+ expect(findCiIcon().classes('borderless')).toBe(true);
+ });
+
+ it('should render a ci-icon with a custom border class', () => {
+ expect(findCiIcon().exists()).toBe(true);
+ expect(findCiIcon().classes('gl-border')).toBe(true);
+ });
});
describe('when update dropdown is changed', () => {
@@ -97,6 +133,7 @@ describe('Pipelines stage component', () => {
it('should render the received data and emit `clickedDropdown` event', async () => {
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
+ expect(findDropdownMenuTitle().text()).toContain(stageReply.name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 06fd970778c..fd97c2dbe77 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -47,17 +47,12 @@ describe('Linked pipeline', () => {
const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
- const createWrapper = ({ propsData, downstreamRetryAction = false }) => {
+ const createWrapper = ({ propsData }) => {
const mockApollo = createMockApollo();
wrapper = extendedWrapper(
mount(LinkedPipelineComponent, {
propsData,
- provide: {
- glFeatures: {
- downstreamRetryAction,
- },
- },
apolloProvider: mockApollo,
}),
);
@@ -164,197 +159,188 @@ describe('Linked pipeline', () => {
});
describe('action button', () => {
- describe('with the `downstream_retry_action` flag on', () => {
- describe('with permissions', () => {
- describe('on an upstream', () => {
- describe('when retryable', () => {
- beforeEach(() => {
- const retryablePipeline = {
- ...upstreamProps,
- pipeline: { ...mockPipeline, retryable: true },
- };
-
- createWrapper({ propsData: retryablePipeline, downstreamRetryAction: true });
- });
+ describe('with permissions', () => {
+ describe('on an upstream', () => {
+ describe('when retryable', () => {
+ beforeEach(() => {
+ const retryablePipeline = {
+ ...upstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
+
+ createWrapper({ propsData: retryablePipeline });
+ });
- it('does not show the retry or cancel button', () => {
- expect(findCancelButton().exists()).toBe(false);
- expect(findRetryButton().exists()).toBe(false);
- });
+ it('does not show the retry or cancel button', () => {
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(false);
});
});
+ });
- describe('on a downstream', () => {
- describe('when retryable', () => {
- beforeEach(() => {
- const retryablePipeline = {
- ...downstreamProps,
- pipeline: { ...mockPipeline, retryable: true },
- };
+ describe('on a downstream', () => {
+ describe('when retryable', () => {
+ beforeEach(() => {
+ const retryablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
- createWrapper({ propsData: retryablePipeline, downstreamRetryAction: true });
- });
+ createWrapper({ propsData: retryablePipeline });
+ });
- it('shows only the retry button', () => {
- expect(findCancelButton().exists()).toBe(false);
- expect(findRetryButton().exists()).toBe(true);
- });
+ it('shows only the retry button', () => {
+ expect(findCancelButton().exists()).toBe(false);
+ expect(findRetryButton().exists()).toBe(true);
+ });
- it('hides the card tooltip when the action button tooltip is hovered', async () => {
- expect(findCardTooltip().exists()).toBe(true);
+ it.each`
+ findElement | name
+ ${findRetryButton} | ${'retry button'}
+ ${findExpandButton} | ${'expand button'}
+ `('hides the card tooltip when $name is hovered', async ({ findElement }) => {
+ expect(findCardTooltip().exists()).toBe(true);
- await findRetryButton().trigger('mouseover');
+ await findElement().trigger('mouseover');
- expect(findCardTooltip().exists()).toBe(false);
- });
+ expect(findCardTooltip().exists()).toBe(false);
+ });
- describe('and the retry button is clicked', () => {
- describe('on success', () => {
- beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- jest.spyOn(wrapper.vm, '$emit');
- await findRetryButton().trigger('click');
- });
+ describe('and the retry button is clicked', () => {
+ describe('on success', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ jest.spyOn(wrapper.vm, '$emit');
+ await findRetryButton().trigger('click');
+ });
- it('calls the retry mutation ', () => {
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: RetryPipelineMutation,
- variables: {
- id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
- },
- });
+ it('calls the retry mutation ', () => {
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: RetryPipelineMutation,
+ variables: {
+ id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ },
});
+ });
- it('emits the refreshPipelineGraph event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
- });
+ it('emits the refreshPipelineGraph event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
});
+ });
- describe('on failure', () => {
- beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
- jest.spyOn(wrapper.vm, '$emit');
- await findRetryButton().trigger('click');
- });
+ describe('on failure', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
+ jest.spyOn(wrapper.vm, '$emit');
+ await findRetryButton().trigger('click');
+ });
- it('emits an error event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
- type: ACTION_FAILURE,
- });
+ it('emits an error event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
+ type: ACTION_FAILURE,
});
});
});
});
+ });
- describe('when cancelable', () => {
- beforeEach(() => {
- const cancelablePipeline = {
- ...downstreamProps,
- pipeline: { ...mockPipeline, cancelable: true },
- };
+ describe('when cancelable', () => {
+ beforeEach(() => {
+ const cancelablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true },
+ };
- createWrapper({ propsData: cancelablePipeline, downstreamRetryAction: true });
- });
+ createWrapper({ propsData: cancelablePipeline });
+ });
- it('shows only the cancel button ', () => {
- expect(findCancelButton().exists()).toBe(true);
- expect(findRetryButton().exists()).toBe(false);
- });
+ it('shows only the cancel button ', () => {
+ expect(findCancelButton().exists()).toBe(true);
+ expect(findRetryButton().exists()).toBe(false);
+ });
- it('hides the card tooltip when the action button tooltip is hovered', async () => {
- expect(findCardTooltip().exists()).toBe(true);
+ it.each`
+ findElement | name
+ ${findCancelButton} | ${'cancel button'}
+ ${findExpandButton} | ${'expand button'}
+ `('hides the card tooltip when $name is hovered', async ({ findElement }) => {
+ expect(findCardTooltip().exists()).toBe(true);
- await findCancelButton().trigger('mouseover');
+ await findElement().trigger('mouseover');
- expect(findCardTooltip().exists()).toBe(false);
- });
+ expect(findCardTooltip().exists()).toBe(false);
+ });
- describe('and the cancel button is clicked', () => {
- describe('on success', () => {
- beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- jest.spyOn(wrapper.vm, '$emit');
- await findCancelButton().trigger('click');
- });
+ describe('and the cancel button is clicked', () => {
+ describe('on success', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ jest.spyOn(wrapper.vm, '$emit');
+ await findCancelButton().trigger('click');
+ });
- it('calls the cancel mutation', () => {
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: CancelPipelineMutation,
- variables: {
- id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
- },
- });
- });
- it('emits the refreshPipelineGraph event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ it('calls the cancel mutation', () => {
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: CancelPipelineMutation,
+ variables: {
+ id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ },
});
});
- describe('on failure', () => {
- beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
- jest.spyOn(wrapper.vm, '$emit');
- await findCancelButton().trigger('click');
- });
- it('emits an error event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
- type: ACTION_FAILURE,
- });
+ it('emits the refreshPipelineGraph event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ });
+ });
+ describe('on failure', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
+ jest.spyOn(wrapper.vm, '$emit');
+ await findCancelButton().trigger('click');
+ });
+ it('emits an error event', () => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
+ type: ACTION_FAILURE,
});
});
});
});
+ });
- describe('when both cancellable and retryable', () => {
- beforeEach(() => {
- const pipelineWithTwoActions = {
- ...downstreamProps,
- pipeline: { ...mockPipeline, cancelable: true, retryable: true },
- };
-
- createWrapper({ propsData: pipelineWithTwoActions, downstreamRetryAction: true });
- });
+ describe('when both cancellable and retryable', () => {
+ beforeEach(() => {
+ const pipelineWithTwoActions = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true, retryable: true },
+ };
- it('only shows the cancel button', () => {
- expect(findRetryButton().exists()).toBe(false);
- expect(findCancelButton().exists()).toBe(true);
- });
+ createWrapper({ propsData: pipelineWithTwoActions });
});
- });
- });
-
- describe('without permissions', () => {
- beforeEach(() => {
- const pipelineWithTwoActions = {
- ...downstreamProps,
- pipeline: {
- ...mockPipeline,
- cancelable: true,
- retryable: true,
- userPermissions: { updatePipeline: false },
- },
- };
-
- createWrapper({ propsData: pipelineWithTwoActions });
- });
- it('does not show any action button', () => {
- expect(findRetryButton().exists()).toBe(false);
- expect(findCancelButton().exists()).toBe(false);
+ it('only shows the cancel button', () => {
+ expect(findRetryButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(true);
+ });
});
});
});
- describe('with the `downstream_retry_action` flag off', () => {
+ describe('without permissions', () => {
beforeEach(() => {
const pipelineWithTwoActions = {
...downstreamProps,
- pipeline: { ...mockPipeline, cancelable: true, retryable: true },
+ pipeline: {
+ ...mockPipeline,
+ cancelable: true,
+ retryable: true,
+ userPermissions: { updatePipeline: false },
+ },
};
createWrapper({ propsData: pipelineWithTwoActions });
});
+
it('does not show any action button', () => {
expect(findRetryButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
@@ -365,19 +351,44 @@ describe('Linked pipeline', () => {
describe('expand button', () => {
it.each`
- pipelineType | anglePosition | buttonBorderClasses | expanded
- ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-0!'} | ${false}
- ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-0!'} | ${true}
- ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-0!'} | ${false}
- ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-0!'} | ${true}
+ pipelineType | chevronPosition | buttonBorderClasses | expanded
+ ${downstreamProps} | ${'chevron-lg-right'} | ${'gl-border-l-0!'} | ${false}
+ ${downstreamProps} | ${'chevron-lg-left'} | ${'gl-border-l-0!'} | ${true}
+ ${upstreamProps} | ${'chevron-lg-left'} | ${'gl-border-r-0!'} | ${false}
+ ${upstreamProps} | ${'chevron-lg-right'} | ${'gl-border-r-0!'} | ${true}
`(
- '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $buttonBorderClasses if expanded state is $expanded',
- ({ pipelineType, anglePosition, buttonBorderClasses, expanded }) => {
+ '$pipelineType.columnTitle pipeline button icon should be $chevronPosition with $buttonBorderClasses if expanded state is $expanded',
+ ({ pipelineType, chevronPosition, buttonBorderClasses, expanded }) => {
createWrapper({ propsData: { ...pipelineType, expanded } });
- expect(findExpandButton().props('icon')).toBe(anglePosition);
+ expect(findExpandButton().props('icon')).toBe(chevronPosition);
expect(findExpandButton().classes()).toContain(buttonBorderClasses);
},
);
+
+ describe('shadow border', () => {
+ beforeEach(() => {
+ createWrapper({ propsData: downstreamProps });
+ });
+
+ it.each`
+ activateEventName | deactivateEventName
+ ${'mouseover'} | ${'mouseout'}
+ ${'focus'} | ${'blur'}
+ `(
+ 'applies the class on $activateEventName and removes it on $deactivateEventName ',
+ async ({ activateEventName, deactivateEventName }) => {
+ const shadowClass = 'gl-shadow-none!';
+
+ expect(findExpandButton().classes()).toContain(shadowClass);
+
+ await findExpandButton().vm.$emit(activateEventName);
+ expect(findExpandButton().classes()).not.toContain(shadowClass);
+
+ await findExpandButton().vm.$emit(deactivateEventName);
+ expect(findExpandButton().classes()).toContain(shadowClass);
+ },
+ );
+ });
});
describe('when isLoading is true', () => {
diff --git a/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js b/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js
deleted file mode 100644
index f626652a944..00000000000
--- a/spec/frontend/pipelines/notification/deprecated_type_keyword_notification_spec.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import VueApollo from 'vue-apollo';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlAlert, GlSprintf } from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import DeprecatedTypeKeywordNotification from '~/pipelines/components/notification/deprecated_type_keyword_notification.vue';
-import getPipelineWarnings from '~/pipelines/graphql/queries/get_pipeline_warnings.query.graphql';
-import {
- mockWarningsWithoutDeprecation,
- mockWarningsRootType,
- mockWarningsType,
- mockWarningsTypesAll,
-} from './mock_data';
-
-const defaultProvide = {
- deprecatedKeywordsDocPath: '/help/ci/yaml/index.md#deprecated-keywords',
- fullPath: '/namespace/my-project',
- pipelineIid: 4,
-};
-
-let wrapper;
-
-const mockWarnings = jest.fn();
-
-const createComponent = ({ isLoading = false, options = {} } = {}) => {
- return shallowMount(DeprecatedTypeKeywordNotification, {
- stubs: {
- GlSprintf,
- },
- provide: {
- ...defaultProvide,
- },
- mocks: {
- $apollo: {
- queries: {
- warnings: {
- loading: isLoading,
- },
- },
- },
- },
- ...options,
- });
-};
-
-const createComponentWithApollo = () => {
- const localVue = createLocalVue();
- localVue.use(VueApollo);
-
- const handlers = [[getPipelineWarnings, mockWarnings]];
- const mockApollo = createMockApollo(handlers);
-
- return createComponent({
- options: {
- localVue,
- apolloProvider: mockApollo,
- mocks: {},
- },
- });
-};
-
-const findAlert = () => wrapper.findComponent(GlAlert);
-const findAlertItems = () => findAlert().findAll('li');
-
-afterEach(() => {
- wrapper.destroy();
-});
-
-describe('Deprecated keyword notification', () => {
- describe('while loading the pipeline warnings', () => {
- beforeEach(() => {
- wrapper = createComponent({ isLoading: true });
- });
-
- it('does not display the notification', () => {
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- describe('if there is an error in the query', () => {
- beforeEach(async () => {
- mockWarnings.mockResolvedValue({ errors: ['It didnt work'] });
- wrapper = createComponentWithApollo();
- await waitForPromises();
- });
-
- it('does not display the notification', () => {
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- describe('with a valid query result', () => {
- describe('if there are no deprecation warnings', () => {
- beforeEach(async () => {
- mockWarnings.mockResolvedValue(mockWarningsWithoutDeprecation);
- wrapper = createComponentWithApollo();
- await waitForPromises();
- });
- it('does not show the notification', () => {
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- describe('with a root type deprecation message', () => {
- beforeEach(async () => {
- mockWarnings.mockResolvedValue(mockWarningsRootType);
- wrapper = createComponentWithApollo();
- await waitForPromises();
- });
- it('shows the notification with one item', () => {
- expect(findAlert().exists()).toBe(true);
- expect(findAlertItems()).toHaveLength(1);
- expect(findAlertItems().at(0).text()).toContain('types');
- });
- });
-
- describe('with a job type deprecation message', () => {
- beforeEach(async () => {
- mockWarnings.mockResolvedValue(mockWarningsType);
- wrapper = createComponentWithApollo();
- await waitForPromises();
- });
- it('shows the notification with one item', () => {
- expect(findAlert().exists()).toBe(true);
- expect(findAlertItems()).toHaveLength(1);
- expect(findAlertItems().at(0).text()).toContain('type');
- expect(findAlertItems().at(0).text()).not.toContain('types');
- });
- });
-
- describe('with both the root types and job type deprecation message', () => {
- beforeEach(async () => {
- mockWarnings.mockResolvedValue(mockWarningsTypesAll);
- wrapper = createComponentWithApollo();
- await waitForPromises();
- });
- it('shows the notification with two items', () => {
- expect(findAlert().exists()).toBe(true);
- expect(findAlertItems()).toHaveLength(2);
- expect(findAlertItems().at(0).text()).toContain('types');
- expect(findAlertItems().at(1).text()).toContain('type');
- expect(findAlertItems().at(1).text()).not.toContain('types');
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/pipeline_tabs_spec.js b/spec/frontend/pipelines/pipeline_tabs_spec.js
new file mode 100644
index 00000000000..b184ce31d20
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_tabs_spec.js
@@ -0,0 +1,95 @@
+import { createAppOptions, createPipelineTabs } from '~/pipelines/pipeline_tabs';
+import { updateHistory } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ removeParams: () => 'gitlab.com',
+ updateHistory: jest.fn(),
+ joinPaths: () => {},
+ setUrlFragment: () => {},
+}));
+
+jest.mock('~/pipelines/utils', () => ({
+ getPipelineDefaultTab: () => '',
+}));
+
+describe('~/pipelines/pipeline_tabs.js', () => {
+ describe('createAppOptions', () => {
+ const SELECTOR = 'SELECTOR';
+
+ let el;
+
+ const createElement = () => {
+ el = document.createElement('div');
+ el.id = SELECTOR;
+ el.dataset.canGenerateCodequalityReports = 'true';
+ el.dataset.codequalityReportDownloadPath = 'codequalityReportDownloadPath';
+ el.dataset.downloadablePathForReportType = 'downloadablePathForReportType';
+ el.dataset.exposeSecurityDashboard = 'true';
+ el.dataset.exposeLicenseScanningData = 'true';
+ el.dataset.failedJobsCount = 1;
+ el.dataset.failedJobsSummary = '[]';
+ el.dataset.graphqlResourceEtag = 'graphqlResourceEtag';
+ el.dataset.pipelineIid = '123';
+ el.dataset.pipelineProjectPath = 'pipelineProjectPath';
+
+ document.body.appendChild(el);
+ };
+
+ afterEach(() => {
+ el = null;
+ });
+
+ it("extracts the properties from the element's dataset", () => {
+ createElement();
+ const options = createAppOptions(`#${SELECTOR}`, null);
+
+ expect(options).toMatchObject({
+ el,
+ provide: {
+ canGenerateCodequalityReports: true,
+ codequalityReportDownloadPath: 'codequalityReportDownloadPath',
+ downloadablePathForReportType: 'downloadablePathForReportType',
+ exposeSecurityDashboard: true,
+ exposeLicenseScanningData: true,
+ failedJobsCount: '1',
+ failedJobsSummary: [],
+ graphqlResourceEtag: 'graphqlResourceEtag',
+ pipelineIid: '123',
+ pipelineProjectPath: 'pipelineProjectPath',
+ },
+ });
+ });
+
+ it('returns `null` if el does not exist', () => {
+ 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/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
index 4b33c1522a5..29c07e5e9f8 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
@@ -1,4 +1,4 @@
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
@@ -9,6 +9,8 @@ describe('Test case details', () => {
const defaultTestCase = {
classname: 'spec.test_spec',
name: 'Test#something cool',
+ file: '~/index.js',
+ filePath: '/src/javascripts/index.js',
formattedTime: '10.04ms',
recent_failures: {
count: 2,
@@ -19,6 +21,8 @@ describe('Test case details', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findName = () => wrapper.findByTestId('test-case-name');
+ const findFile = () => wrapper.findByTestId('test-case-file');
+ const findFileLink = () => wrapper.findComponent(GlLink);
const findDuration = () => wrapper.findByTestId('test-case-duration');
const findRecentFailures = () => wrapper.findByTestId('test-case-recent-failures');
const findAttachmentUrl = () => wrapper.findByTestId('test-case-attachment-url');
@@ -57,11 +61,26 @@ describe('Test case details', () => {
expect(findName().text()).toBe(defaultTestCase.name);
});
+ it('renders the test case file', () => {
+ expect(findFile().text()).toBe(defaultTestCase.file);
+ expect(findFileLink().attributes('href')).toBe(defaultTestCase.filePath);
+ });
+
it('renders the test case duration', () => {
expect(findDuration().text()).toBe(defaultTestCase.formattedTime);
});
});
+ describe('when test case has execution time instead of formatted time', () => {
+ beforeEach(() => {
+ createComponent({ ...defaultTestCase, formattedTime: null, execution_time: 17 });
+ });
+
+ it('renders the test case duration', () => {
+ expect(findDuration().text()).toBe('17 s');
+ });
+ });
+
describe('when test case has recent failures', () => {
describe('has only 1 recent failure', () => {
it('renders the recent failure', () => {
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index dc72fa31ace..25650b24705 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,9 +1,9 @@
-import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
+import { GlButton, GlFriendlyWrap, GlLink, GlPagination, GlEmptyState } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
+import SuiteTable, { i18n } from '~/pipelines/components/test_reports/test_suite_table.vue';
import { TestStatus } from '~/pipelines/constants';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
@@ -26,6 +26,7 @@ describe('Test reports suite table', () => {
const noCasesMessage = () => wrapper.findByTestId('no-test-cases');
const artifactsExpiredMessage = () => wrapper.findByTestId('artifacts-expired');
+ const artifactsExpiredEmptyState = () => wrapper.find(GlEmptyState);
const allCaseRows = () => wrapper.findAllByTestId('test-case-row');
const findCaseRowAtIndex = (index) => wrapper.findAllByTestId('test-case-row').at(index);
const findLinkForRow = (row) => row.find(GlLink);
@@ -65,11 +66,15 @@ describe('Test reports suite table', () => {
expect(artifactsExpiredMessage().exists()).toBe(false);
});
- it('should render a message when artifacts have expired', () => {
+ it('should render an empty state when artifacts have expired', () => {
createComponent({ suite: [], errorMessage: ARTIFACTS_EXPIRED_ERROR_MESSAGE });
+ const emptyState = artifactsExpiredEmptyState();
- expect(noCasesMessage().exists()).toBe(true);
+ expect(noCasesMessage().exists()).toBe(false);
expect(artifactsExpiredMessage().exists()).toBe(true);
+
+ expect(emptyState.exists()).toBe(true);
+ expect(emptyState.props('title')).toBe(i18n.expiredArtifactsTitle);
});
describe('when a test suite is supplied', () => {
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index e342b7c4ba1..0e56bccf27e 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -52,7 +52,7 @@ describe('UpdateUsername component', () => {
openModalBtn: wrapper.find('[data-testid="username-change-confirmation-modal"]'),
modalBody: modal.find('.modal-body'),
modalHeader: modal.find('.modal-title'),
- confirmModalBtn: wrapper.find('.btn-warning'),
+ confirmModalBtn: wrapper.find('.btn-confirm'),
};
};
diff --git a/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
new file mode 100644
index 00000000000..d230b96ad82
--- /dev/null
+++ b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
@@ -0,0 +1,45 @@
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ClustersDeprecationAlert from '~/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue';
+
+const message = 'Alert message';
+
+describe('ClustersDeprecationAlert', () => {
+ let wrapper;
+
+ const provideData = {
+ message,
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createComponent = () => {
+ wrapper = shallowMount(ClustersDeprecationAlert, {
+ provide: provideData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('should render a non-dismissible warning alert', () => {
+ expect(findAlert().props()).toMatchObject({
+ dismissible: false,
+ variant: 'warning',
+ });
+ });
+
+ it('should display the correct message', () => {
+ expect(findAlert().text()).toBe(message);
+ });
+ });
+});
diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js
index 57906045337..a741393fcf3 100644
--- a/spec/frontend/projects/compare/components/revision_card_spec.js
+++ b/spec/frontend/projects/compare/components/revision_card_spec.js
@@ -1,4 +1,3 @@
-import { GlCard } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RepoDropdown from '~/projects/compare/components/repo_dropdown.vue';
import RevisionCard from '~/projects/compare/components/revision_card.vue';
@@ -14,9 +13,6 @@ describe('RepoDropdown component', () => {
...defaultProps,
...props,
},
- stubs: {
- GlCard,
- },
});
};
@@ -29,8 +25,10 @@ describe('RepoDropdown component', () => {
createComponent();
});
+ const RevisionCardWrapper = () => wrapper.find('.revision-card');
+
it('displays revision text', () => {
- expect(wrapper.find(GlCard).text()).toContain(defaultProps.revisionText);
+ expect(RevisionCardWrapper().text()).toContain(defaultProps.revisionText);
});
it('renders RepoDropdown component', () => {
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 42259a5c392..f50dd393174 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
@@ -57,7 +57,7 @@ describe('New project push tip popover', () => {
});
expect(findFormInput().attributes()).toMatchObject({
'aria-label': 'Push project from command line',
- readonly: 'readonly',
+ readonly: '',
});
});
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 9c94925c817..98c7856a61a 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -13,6 +13,7 @@ jest.mock('~/lib/utils/url_utility');
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} };
+const TimeToRestoreServiceChartsStub = { name: 'TimeToRestoreServiceCharts', render: () => {} };
const ProjectQualitySummaryStub = { name: 'ProjectQualitySummary', render: () => {} };
describe('ProjectsPipelinesChartsApp', () => {
@@ -31,6 +32,7 @@ describe('ProjectsPipelinesChartsApp', () => {
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
LeadTimeCharts: LeadTimeChartsStub,
+ TimeToRestoreServiceCharts: TimeToRestoreServiceChartsStub,
ProjectQualitySummary: ProjectQualitySummaryStub,
},
},
@@ -47,6 +49,7 @@ describe('ProjectsPipelinesChartsApp', () => {
const findAllGlTabs = () => wrapper.findAll(GlTab);
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub);
+ const findTimeToRestoreServiceCharts = () => wrapper.find(TimeToRestoreServiceChartsStub);
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
const findPipelineCharts = () => wrapper.find(PipelineCharts);
const findProjectQualitySummary = () => wrapper.find(ProjectQualitySummaryStub);
@@ -62,6 +65,7 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe('Pipelines');
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
+ expect(findGlTabAtIndex(3).attributes('title')).toBe('Time to restore service');
});
it('renders the pipeline charts', () => {
@@ -76,6 +80,10 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findLeadTimeCharts().exists()).toBe(true);
});
+ it('renders the time to restore service charts', () => {
+ expect(findTimeToRestoreServiceCharts().exists()).toBe(true);
+ });
+
it('renders the project quality summary', () => {
expect(findProjectQualitySummary().exists()).toBe(true);
});
@@ -123,10 +131,11 @@ 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'}
+ 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'}
`('tracks the $event event when clicked', ({ testId, event }) => {
jest.spyOn(API, 'trackRedisHllUserEvent');
@@ -141,12 +150,13 @@ describe('ProjectsPipelinesChartsApp', () => {
describe('when provided with a query param', () => {
it.each`
- chart | tab
- ${'lead-time'} | ${'2'}
- ${'deployment-frequency'} | ${'1'}
- ${'pipelines'} | ${'0'}
- ${'fake'} | ${'0'}
- ${''} | ${'0'}
+ chart | tab
+ ${'time-to-restore-service'} | ${'3'}
+ ${'lead-time'} | ${'2'}
+ ${'deployment-frequency'} | ${'1'}
+ ${'pipelines'} | ${'0'}
+ ${'fake'} | ${'0'}
+ ${''} | ${'0'}
`('shows the correct tab for URL parameter "$chart"', ({ chart, tab }) => {
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts?chart=${chart}`);
getParameterValues.mockImplementation((name) => {
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index fe325343da8..3034037fb1d 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import projectNew from '~/projects/project_new';
@@ -8,6 +7,9 @@ describe('New Project', () => {
let $projectPath;
let $projectName;
+ const mockKeyup = (el) => el.dispatchEvent(new KeyboardEvent('keyup'));
+ const mockChange = (el) => el.dispatchEvent(new Event('change'));
+
beforeEach(() => {
setHTMLFixture(`
<div class='toggle-import-form'>
@@ -29,122 +31,127 @@ describe('New Project', () => {
</div>
`);
- $projectImportUrl = $('#project_import_url');
- $projectPath = $('#project_path');
- $projectName = $('#project_name');
+ $projectImportUrl = document.querySelector('#project_import_url');
+ $projectPath = document.querySelector('#project_path');
+ $projectName = document.querySelector('#project_name');
});
afterEach(() => {
resetHTMLFixture();
});
+ const setValueAndTriggerEvent = (el, value, event) => {
+ event(el);
+ el.value = value;
+ };
+
describe('deriveProjectPathFromUrl', () => {
const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`;
beforeEach(() => {
projectNew.bindEvents();
- $projectPath.val('').keyup().val(dummyImportUrl);
+ setValueAndTriggerEvent($projectPath, dummyImportUrl, mockKeyup);
});
it('does not change project path for disabled $projectImportUrl', () => {
- $projectImportUrl.prop('disabled', true);
+ $projectImportUrl.setAttribute('disabled', true);
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual(dummyImportUrl);
+ expect($projectPath.value).toEqual(dummyImportUrl);
});
describe('for enabled $projectImportUrl', () => {
beforeEach(() => {
- $projectImportUrl.prop('disabled', false);
+ $projectImportUrl.setAttribute('disabled', false);
});
it('does not change project path if it is set by user', () => {
- $projectPath.keyup();
+ mockKeyup($projectPath);
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual(dummyImportUrl);
+ expect($projectPath.value).toEqual(dummyImportUrl);
});
it('does not change project path for empty $projectImportUrl', () => {
- $projectImportUrl.val('');
+ $projectImportUrl.value = '';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual(dummyImportUrl);
+ expect($projectPath.value).toEqual(dummyImportUrl);
});
it('does not change project path for whitespace $projectImportUrl', () => {
- $projectImportUrl.val(' ');
+ $projectImportUrl.value = ' ';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual(dummyImportUrl);
+ expect($projectPath.value).toEqual(dummyImportUrl);
});
it('does not change project path for $projectImportUrl without slashes', () => {
- $projectImportUrl.val('has-no-slash');
+ $projectImportUrl.value = 'has-no-slash';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual(dummyImportUrl);
+ expect($projectPath.value).toEqual(dummyImportUrl);
});
it('changes project path to last $projectImportUrl component', () => {
- $projectImportUrl.val('/this/is/last');
+ $projectImportUrl.value = '/this/is/last';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual('last');
+ expect($projectPath.value).toEqual('last');
});
it('ignores trailing slashes in $projectImportUrl', () => {
- $projectImportUrl.val('/has/trailing/slash/');
+ $projectImportUrl.value = '/has/trailing/slash/';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual('slash');
+ expect($projectPath.value).toEqual('slash');
});
it('ignores fragment identifier in $projectImportUrl', () => {
- $projectImportUrl.val('/this/has/a#fragment-identifier/');
+ $projectImportUrl.value = '/this/has/a#fragment-identifier/';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual('a');
+ expect($projectPath.value).toEqual('a');
});
it('ignores query string in $projectImportUrl', () => {
- $projectImportUrl.val('/url/with?query=string');
+ $projectImportUrl.value = '/url/with?query=string';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual('with');
+ expect($projectPath.value).toEqual('with');
});
it('ignores trailing .git in $projectImportUrl', () => {
- $projectImportUrl.val('/repository.git');
+ $projectImportUrl.value = '/repository.git';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual('repository');
+ expect($projectPath.value).toEqual('repository');
});
it('changes project path for HTTPS URL in $projectImportUrl', () => {
- $projectImportUrl.val('https://gitlab.company.com/group/project.git');
+ $projectImportUrl.value = 'https://gitlab.company.com/group/project.git';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual('project');
+ expect($projectPath.value).toEqual('project');
});
it('changes project path for SSH URL in $projectImportUrl', () => {
- $projectImportUrl.val('git@gitlab.com:gitlab-org/gitlab-ce.git');
+ $projectImportUrl.value = 'git@gitlab.com:gitlab-org/gitlab-ce.git';
projectNew.deriveProjectPathFromUrl($projectImportUrl);
- expect($projectPath.val()).toEqual('gitlab-ce');
+ expect($projectPath.value).toEqual('gitlab-ce');
});
});
});
@@ -152,27 +159,27 @@ describe('New Project', () => {
describe('deriveSlugFromProjectName', () => {
beforeEach(() => {
projectNew.bindEvents();
- $projectName.val('').keyup();
+ setValueAndTriggerEvent($projectName, '', mockKeyup);
});
it('converts project name to lower case and dash-limited slug', () => {
const dummyProjectName = 'My Awesome Project';
- $projectName.val(dummyProjectName);
+ $projectName.value = dummyProjectName;
projectNew.onProjectNameChange($projectName, $projectPath);
- expect($projectPath.val()).toEqual('my-awesome-project');
+ expect($projectPath.value).toEqual('my-awesome-project');
});
it('does not add additional dashes in the slug if the project name already contains dashes', () => {
const dummyProjectName = 'My-Dash-Delimited Awesome Project';
- $projectName.val(dummyProjectName);
+ $projectName.value = dummyProjectName;
projectNew.onProjectNameChange($projectName, $projectPath);
- expect($projectPath.val()).toEqual('my-dash-delimited-awesome-project');
+ expect($projectPath.value).toEqual('my-dash-delimited-awesome-project');
});
});
@@ -182,27 +189,28 @@ describe('New Project', () => {
beforeEach(() => {
projectNew.bindEvents();
- $projectPath.val('').change();
+ setValueAndTriggerEvent($projectPath, '', mockChange);
});
it('converts slug to humanized project name', () => {
- $projectPath.val(dummyProjectPath);
+ $projectPath.value = dummyProjectPath;
+ mockChange($projectPath);
projectNew.onProjectPathChange($projectName, $projectPath);
- expect($projectName.val()).toEqual('My Awesome Project');
+ expect($projectName.value).toEqual('My Awesome Project');
});
it('does not convert slug to humanized project name if a project name already exists', () => {
- $projectName.val(dummyProjectName);
- $projectPath.val(dummyProjectPath);
+ $projectName.value = dummyProjectName;
+ $projectPath.value = dummyProjectPath;
projectNew.onProjectPathChange(
$projectName,
$projectPath,
- $projectName.val().trim().length > 0,
+ $projectName.value.trim().length > 0,
);
- expect($projectName.val()).toEqual(dummyProjectName);
+ expect($projectName.value).toEqual(dummyProjectName);
});
});
});
diff --git a/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js
new file mode 100644
index 00000000000..5997c2a083c
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/branch_dropdown_spec.js
@@ -0,0 +1,101 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import BranchDropdown, {
+ i18n,
+} from '~/projects/settings/branch_rules/components/branch_dropdown.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import branchesQuery from '~/projects/settings/branch_rules/queries/branches.query.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+
+Vue.use(VueApollo);
+jest.mock('~/flash');
+
+describe('Branch dropdown', () => {
+ let wrapper;
+
+ const projectPath = 'test/project';
+ const value = 'main';
+ const mockBranchNames = ['test 1', 'test 2'];
+
+ const createComponent = async ({ branchNames = mockBranchNames, resolver } = {}) => {
+ const mockResolver =
+ resolver ||
+ jest.fn().mockResolvedValue({
+ data: { project: { id: '1', repository: { branchNames } } },
+ });
+ const apolloProvider = createMockApollo([[branchesQuery, mockResolver]]);
+
+ wrapper = shallowMountExtended(BranchDropdown, {
+ apolloProvider,
+ propsData: { projectPath, value },
+ });
+
+ await waitForPromises();
+ };
+
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findAllBranches = () => wrapper.findAll(GlDropdownItem);
+ const findNoDataMsg = () => wrapper.findByTestId('no-data');
+ const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+ const findWildcardButton = () => wrapper.findByTestId('create-wildcard-button');
+ const setSearchTerm = (searchTerm) => findGlSearchBoxByType().vm.$emit('input', searchTerm);
+
+ beforeEach(() => createComponent());
+
+ it('renders a GlDropdown component with the correct props', () => {
+ expect(findGlDropdown().props()).toMatchObject({ text: value });
+ });
+
+ it('renders GlDropdownItem components for each branch', () => {
+ expect(findAllBranches().length).toBe(mockBranchNames.length);
+
+ mockBranchNames.forEach((branchName, index) =>
+ expect(findAllBranches().at(index).text()).toBe(branchName),
+ );
+ });
+
+ it('emits `select` with the branch name when a branch is clicked', () => {
+ findAllBranches().at(0).vm.$emit('click');
+ expect(wrapper.emitted('input')).toEqual([[mockBranchNames[0]]]);
+ });
+
+ describe('branch searching', () => {
+ it('displays a message if no branches can be found', async () => {
+ await createComponent({ branchNames: [] });
+
+ expect(findNoDataMsg().text()).toBe(i18n.noMatch);
+ });
+
+ it('displays a loading state while search request is in flight', async () => {
+ setSearchTerm('test');
+ await nextTick();
+
+ expect(findGlSearchBoxByType().props()).toMatchObject({ isLoading: true });
+ });
+
+ it('renders a wildcard button', async () => {
+ const searchTerm = 'test-*';
+ setSearchTerm(searchTerm);
+ await nextTick();
+
+ expect(findWildcardButton().exists()).toBe(true);
+ findWildcardButton().vm.$emit('click');
+ expect(wrapper.emitted('createWildcard')).toEqual([[searchTerm]]);
+ });
+ });
+
+ it('displays an error message if fetch failed', async () => {
+ const error = new Error('an error occurred');
+ const resolver = jest.fn().mockRejectedValueOnce(error);
+ await createComponent({ resolver });
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.fetchBranchesError,
+ captureError: true,
+ error,
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js b/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js
new file mode 100644
index 00000000000..66ae6ddc02d
--- /dev/null
+++ b/spec/frontend/projects/settings/branch_rules/rule_edit_spec.js
@@ -0,0 +1,49 @@
+import { nextTick } from 'vue';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RuleEdit from '~/projects/settings/branch_rules/components/rule_edit.vue';
+import BranchDropdown from '~/projects/settings/branch_rules/components/branch_dropdown.vue';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ getParameterByName: jest.fn().mockImplementation(() => 'main'),
+}));
+
+describe('Edit branch rule', () => {
+ let wrapper;
+ const projectPath = 'test/testing';
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(RuleEdit, { propsData: { projectPath } });
+ };
+
+ const findBranchDropdown = () => wrapper.find(BranchDropdown);
+
+ beforeEach(() => createComponent());
+
+ it('gets the branch param from url', () => {
+ expect(getParameterByName).toHaveBeenCalledWith('branch');
+ });
+
+ describe('BranchDropdown', () => {
+ it('renders a BranchDropdown component with the correct props', () => {
+ expect(findBranchDropdown().props()).toMatchObject({
+ projectPath,
+ value: 'main',
+ });
+ });
+
+ it('sets the correct value when `input` is emitted', async () => {
+ const branch = 'test';
+ findBranchDropdown().vm.$emit('input', branch);
+ await nextTick();
+ expect(findBranchDropdown().props('value')).toBe(branch);
+ });
+
+ it('sets the correct value when `createWildcard` is emitted', async () => {
+ const wildcard = 'test-*';
+ findBranchDropdown().vm.$emit('createWildcard', wildcard);
+ await nextTick();
+ expect(findBranchDropdown().props('value')).toBe(wildcard);
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
new file mode 100644
index 00000000000..e12c3aeedd6
--- /dev/null
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -0,0 +1,18 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
+
+describe('Branch rules app', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mountExtended(BranchRules);
+ };
+
+ const findTitle = () => wrapper.find('strong');
+
+ beforeEach(() => createComponent());
+
+ it('renders a title', () => {
+ expect(findTitle().text()).toBe('Branch');
+ });
+});
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 473327bf5e1..fc906194059 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -6,9 +6,9 @@ import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import { metrics1 as metrics } from './mock_data';
describe('PrometheusMetrics', () => {
- const FIXTURE = 'services/prometheus/prometheus_service.html';
+ const FIXTURE = 'integrations/prometheus/prometheus_integration.html';
const customMetricsEndpoint =
- 'http://test.host/frontend-fixtures/services-project/prometheus/metrics';
+ 'http://test.host/frontend-fixtures/integrations-project/prometheus/metrics';
let mock;
beforeEach(() => {
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index 1151c0b3769..0df2aad5882 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -7,7 +7,7 @@ import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import { metrics2 as metrics, missingVarMetrics } from './mock_data';
describe('PrometheusMetrics', () => {
- const FIXTURE = 'services/prometheus/prometheus_service.html';
+ const FIXTURE = 'integrations/prometheus/prometheus_integration.html';
beforeEach(() => {
loadHTMLFixture(FIXTURE);
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 2ab4afbffbe..d498b6f0c4f 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -22,7 +22,7 @@ import userInfoQuery from '~/repository/queries/user_info.query.graphql';
import applicationInfoQuery from '~/repository/queries/application_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import { redirectTo } from '~/lib/utils/url_utility';
-import { isLoggedIn } from '~/lib/utils/common_utils';
+import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import LineHighlighter from '~/blob/line_highlighter';
@@ -163,6 +163,14 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
});
+ it('copies blob text to clipboard', async () => {
+ jest.spyOn(navigator.clipboard, 'writeText');
+ await createComponent();
+
+ findBlobHeader().vm.$emit('copy');
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(simpleViewerMock.rawTextBlob);
+ });
+
it('renders a BlobContent component', async () => {
await createComponent();
@@ -209,6 +217,12 @@ describe('Blob content viewer component', () => {
await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } });
expect(LineHighlighter).toHaveBeenCalled();
});
+
+ it('scrolls to the hash', async () => {
+ mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
+ await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } });
+ expect(handleLocationHash).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/repository/components/blob_viewers/sketch_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/sketch_viewer_spec.js
new file mode 100644
index 00000000000..b5c8c02c4a0
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/sketch_viewer_spec.js
@@ -0,0 +1,32 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SketchViewer from '~/repository/components/blob_viewers/sketch_viewer.vue';
+import SketchLoader from '~/blob/sketch';
+
+jest.mock('~/blob/sketch');
+
+describe('Sketch Viewer', () => {
+ let wrapper;
+
+ const DEFAULT_BLOB_DATA = {
+ rawPath: 'some/file.sketch',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(SketchViewer, {
+ propsData: { blob: DEFAULT_BLOB_DATA },
+ });
+ };
+
+ const findSketchWrapper = () => wrapper.findByTestId('sketch');
+
+ beforeEach(() => createComponent());
+
+ it('inits the sketch loader', () => {
+ expect(SketchLoader).toHaveBeenCalledWith(wrapper.vm.$refs.viewer);
+ });
+
+ it('renders the sketch viewer', () => {
+ expect(findSketchWrapper().exists()).toBe(true);
+ expect(findSketchWrapper().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath);
+ });
+});
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
index fe7f024e3ea..e1c50d63851 100644
--- a/spec/frontend/repository/components/new_directory_modal_spec.js
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -67,7 +67,7 @@ describe('NewDirectoryModal', () => {
await findBranchName().vm.$emit('input', branchName);
await findCommitMessage().vm.$emit('input', commitMessage);
await findMrToggle().vm.$emit('change', createNewMr);
- await nextTick;
+ await nextTick();
};
const submitForm = async () => {
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 07c151ad935..ff0371b5c07 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui';
+import { GlSkeletonLoader, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Table from '~/repository/components/table/index.vue';
@@ -103,7 +103,7 @@ describe('Repository table component', () => {
it('shows loading icon', () => {
factory({ path: '/', isLoading: true });
- expect(vm.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(vm.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
it('renders table rows', () => {
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
index 07259ec3538..28e7d192938 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlTab, GlTabs } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
@@ -11,6 +12,7 @@ import RunnerHeader from '~/runner/components/runner_header.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';
@@ -38,6 +40,8 @@ describe('AdminRunnerShowApp', () => {
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
+ const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
@@ -47,7 +51,7 @@ describe('AdminRunnerShowApp', () => {
});
};
- const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(AdminRunnerShowApp, {
apolloProvider: createMockApollo([[runnerQuery, mockRunnerQuery]]),
propsData: {
@@ -55,6 +59,7 @@ describe('AdminRunnerShowApp', () => {
runnersPath: mockRunnersPath,
...props,
},
+ ...options,
});
return waitForPromises();
@@ -69,7 +74,7 @@ describe('AdminRunnerShowApp', () => {
beforeEach(async () => {
mockRunnerQueryResult();
- await createComponent({ mountFn: mount });
+ await createComponent({ mountFn: mountExtended });
});
it('expect GraphQL ID to be requested', async () => {
@@ -110,7 +115,7 @@ describe('AdminRunnerShowApp', () => {
});
await createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
});
});
@@ -129,7 +134,7 @@ describe('AdminRunnerShowApp', () => {
});
await createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
});
});
@@ -141,7 +146,7 @@ describe('AdminRunnerShowApp', () => {
describe('when runner is deleted', () => {
beforeEach(async () => {
await createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
});
});
@@ -163,7 +168,7 @@ describe('AdminRunnerShowApp', () => {
});
await createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
});
});
@@ -191,4 +196,49 @@ describe('AdminRunnerShowApp', () => {
expect(createAlert).toHaveBeenCalled();
});
});
+
+ describe('Jobs tab', () => {
+ const stubs = {
+ GlTab,
+ GlTabs,
+ RunnerDetails: {
+ template: `
+ <div>
+ <slot name="jobs-tab"></slot>
+ </div>
+ `,
+ },
+ };
+
+ it('without a runner, shows no jobs', () => {
+ mockRunnerQuery = jest.fn().mockResolvedValue({
+ data: {
+ runner: null,
+ },
+ });
+
+ createComponent({ stubs });
+
+ expect(findJobCountBadge().exists()).toBe(false);
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
+
+ it('without a job count, shows no jobs count', async () => {
+ mockRunnerQueryResult({ jobCount: null });
+
+ await createComponent({ stubs });
+
+ expect(findJobCountBadge().exists()).toBe(false);
+ });
+
+ it('with a job count, shows jobs count', async () => {
+ const runner = { jobCount: 3 };
+ mockRunnerQueryResult(runner);
+
+ await createComponent({ stubs });
+
+ expect(findJobCountBadge().text()).toBe('3');
+ expect(findRunnersJobs().props('runner')).toEqual({ ...mockRunner, ...runner });
+ });
+ });
});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 405813be4e3..3d25ad075de 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -18,6 +18,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
+import 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';
@@ -50,6 +51,8 @@ import {
runnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
} from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
@@ -78,6 +81,7 @@ describe('AdminRunnersApp', () => {
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
+ const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
@@ -106,6 +110,8 @@ describe('AdminRunnersApp', () => {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
...provide,
},
...options,
@@ -457,12 +463,28 @@ describe('AdminRunnersApp', () => {
runners: { nodes: [] },
},
});
+
createComponent();
await waitForPromises();
});
- it('shows a message for no results', async () => {
- expect(wrapper.text()).toContain('No runners found');
+ it('shows an empty state', () => {
+ expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false);
+ });
+
+ describe('when a filter is selected by the user', () => {
+ beforeEach(async () => {
+ findRunnerFilteredSearchBar().vm.$emit('input', {
+ runnerType: null,
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
+ sort: CREATED_ASC,
+ });
+ await waitForPromises();
+ });
+
+ it('shows an empty state for a filtered search', () => {
+ expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(true);
+ });
});
});
diff --git a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
index 80a04401760..b27a1adf01b 100644
--- a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
+++ b/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RunnerStatusPopover renders complete text 1`] = `"Never contacted: Runner has never contacted GitLab (when you register a runner, use gitlab-runner run to bring it online) Online: Runner has contacted GitLab within the last 2 hours Offline: Runner has not contacted GitLab in more than 2 hours Stale: Runner has not contacted GitLab in more than 2 months"`;
+exports[`RunnerStatusPopover renders complete text 1`] = `"Never contacted: Runner has never contacted GitLab (when you register a runner, use gitlab-runner run to bring it online) Online: Runner has contacted GitLab within the last 2 hours Offline: Runner has not contacted GitLab in more than 2 hours Stale: Runner has not contacted GitLab in more than 3 months"`;
diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
index 20a1cdf7236..0f5133d0ae2 100644
--- a/spec/frontend/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
@@ -1,12 +1,15 @@
-import { GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import RunnerStatusCell from '~/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 { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
-describe('RunnerTypeCell', () => {
+describe('RunnerStatusCell', () => {
let wrapper;
- const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i);
+ const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
+ const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
const createComponent = ({ runner = {} } = {}) => {
wrapper = mount(RunnerStatusCell, {
@@ -29,7 +32,7 @@ describe('RunnerTypeCell', () => {
createComponent();
expect(wrapper.text()).toMatchInterpolatedText('online');
- expect(findBadgeAt(0).text()).toBe('online');
+ expect(findStatusBadge().text()).toBe('online');
});
it('Displays offline status', () => {
@@ -40,7 +43,7 @@ describe('RunnerTypeCell', () => {
});
expect(wrapper.text()).toMatchInterpolatedText('offline');
- expect(findBadgeAt(0).text()).toBe('offline');
+ expect(findStatusBadge().text()).toBe('offline');
});
it('Displays paused status', () => {
@@ -52,9 +55,7 @@ describe('RunnerTypeCell', () => {
});
expect(wrapper.text()).toMatchInterpolatedText('online paused');
-
- expect(findBadgeAt(0).text()).toBe('online');
- expect(findBadgeAt(1).text()).toBe('paused');
+ expect(findPausedBadge().text()).toBe('paused');
});
it('Is empty when data is missing', () => {
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
index 81c2788f084..d3f38bc1d26 100644
--- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
+import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
import { mount, shallowMount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -24,6 +24,8 @@ import {
const mockToken = '0123456789';
const maskToken = '**********';
+Vue.use(VueApollo);
+
describe('RegistrationDropdown', () => {
let wrapper;
@@ -32,9 +34,11 @@ describe('RegistrationDropdown', () => {
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
- const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input');
+ const findRegistrationTokenInput = () =>
+ wrapper.findByLabelText(RegistrationToken.i18n.registrationToken);
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
+ const findModal = () => wrapper.findComponent(GlModal);
const findModalContent = () =>
createWrapper(document.body)
.find('[data-testid="runner-instructions-modal"]')
@@ -43,6 +47,8 @@ describe('RegistrationDropdown', () => {
const openModal = async () => {
await findRegistrationInstructionsDropdownItem().trigger('click');
+ findModal().vm.$emit('shown');
+
await waitForPromises();
};
@@ -60,8 +66,6 @@ describe('RegistrationDropdown', () => {
};
const createComponentWithModal = () => {
- Vue.use(VueApollo);
-
const requestHandlers = [
[getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
[getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
@@ -169,10 +173,10 @@ describe('RegistrationDropdown', () => {
await nextTick();
};
- it('Updates token in input', async () => {
+ it('Updates token input', async () => {
createComponent({}, mount);
- expect(findRegistrationTokenInput().props('value')).not.toBe(newToken);
+ expect(findRegistrationToken().props('value')).not.toBe(newToken);
await resetToken();
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
index cb42c7c8493..ed1a698d36f 100644
--- a/spec/frontend/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -29,6 +29,7 @@ describe('RegistrationToken', () => {
wrapper = mountFn(RegistrationToken, {
propsData: {
value: mockToken,
+ inputId: 'token-value',
...props,
},
localVue,
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
index 162d21febfd..9e0f7014750 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -1,14 +1,13 @@
-import { GlSprintf, GlIntersperse, GlTab } from '@gitlab/ui';
-import { createWrapper, ErrorWrapper } from '@vue/test-utils';
+import { GlSprintf, GlIntersperse } from '@gitlab/ui';
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 RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerDetail from '~/runner/components/runner_detail.vue';
import RunnerGroups from '~/runner/components/runner_groups.vue';
-import RunnersJobs from '~/runner/components/runner_jobs.vue';
import RunnerTags from '~/runner/components/runner_tags.vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
@@ -24,25 +23,14 @@ describe('RunnerDetails', () => {
useFakeDate(mockNow);
- /**
- * Find the definition (<dd>) that corresponds to this term (<dt>)
- * @param {string} dtLabel - Label for this value
- * @returns Wrapper
- */
- const findDd = (dtLabel) => {
- const dt = wrapper.findByText(dtLabel).element;
- const dd = dt.nextElementSibling;
- if (dt.tagName === 'DT' && dd.tagName === 'DD') {
- return createWrapper(dd, {});
- }
- return ErrorWrapper(dtLabel);
- };
-
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
- const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
- const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
- const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
+ const createComponent = ({
+ props = {},
+ stubs,
+ mountFn = shallowMountExtended,
+ ...options
+ } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
...props,
@@ -51,6 +39,7 @@ describe('RunnerDetails', () => {
RunnerDetail,
...stubs,
},
+ ...options,
});
};
@@ -108,7 +97,7 @@ describe('RunnerDetails', () => {
});
it(`displays expected value "${expectedValue}"`, () => {
- expect(findDd(field).text()).toBe(expectedValue);
+ expect(findDd(field, wrapper).text()).toBe(expectedValue);
});
});
@@ -123,7 +112,7 @@ describe('RunnerDetails', () => {
stubs,
});
- expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
+ expect(findDd('Tags', wrapper).text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
});
it('displays "None" when runner has no tags', () => {
@@ -134,7 +123,7 @@ describe('RunnerDetails', () => {
stubs,
});
- expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
+ expect(findDd('Tags', wrapper).text().replace(/\s+/g, ' ')).toBe('None');
});
});
@@ -153,40 +142,17 @@ describe('RunnerDetails', () => {
});
});
- describe('Jobs tab', () => {
- const stubs = { GlTab };
-
- it('without a runner, shows no jobs', () => {
- createComponent({
- props: { runner: null },
- stubs,
- });
-
- expect(findJobCountBadge().exists()).toBe(false);
- expect(findRunnersJobs().exists()).toBe(false);
- });
+ describe('Jobs tab slot', () => {
+ it('shows job tab slot', () => {
+ const JOBS_TAB = '<div>Jobs Tab</div>';
- it('without a job count, shows no jobs count', () => {
createComponent({
- props: {
- runner: { ...mockRunner, jobCount: undefined },
+ slots: {
+ 'jobs-tab': JOBS_TAB,
},
- stubs,
- });
-
- expect(findJobCountBadge().exists()).toBe(false);
- });
-
- it('with a job count, shows jobs count', () => {
- const runner = { ...mockRunner, jobCount: 3 };
-
- createComponent({
- props: { runner },
- stubs,
});
- expect(findJobCountBadge().text()).toBe('3');
- expect(findRunnersJobs().props('runner')).toBe(runner);
+ expect(wrapper.html()).toContain(JOBS_TAB);
});
});
});
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js
index 8ac5685a0dd..20582aaaf40 100644
--- a/spec/frontend/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/runner/components/runner_jobs_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -28,7 +28,7 @@ describe('RunnerJobs', () => {
let wrapper;
let mockRunnerJobsQuery;
- const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading);
+ const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader);
const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable);
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
diff --git a/spec/frontend/runner/components/runner_list_empty_state_spec.js b/spec/frontend/runner/components/runner_list_empty_state_spec.js
new file mode 100644
index 00000000000..59cff863106
--- /dev/null
+++ b/spec/frontend/runner/components/runner_list_empty_state_spec.js
@@ -0,0 +1,76 @@
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+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';
+
+const mockSvgPath = 'mock-svg-path.svg';
+const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
+
+describe('RunnerListEmptyState', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
+
+ const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(RunnerListEmptyState, {
+ propsData: {
+ svgPath: mockSvgPath,
+ filteredSvgPath: mockFilteredSvgPath,
+ ...props,
+ },
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ stubs: {
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ },
+ });
+ };
+
+ describe('when search is not filtered', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ });
+
+ it('displays "no results" text', () => {
+ const title = s__('Runners|Get started with runners');
+ const desc = s__(
+ 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
+ );
+
+ expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
+ });
+
+ describe('when search is filtered', () => {
+ beforeEach(() => {
+ createComponent({ props: { isSearchFiltered: true } });
+ });
+
+ it('renders a "filtered search" illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(mockFilteredSvgPath);
+ });
+
+ it('displays "no filtered results" text', () => {
+ expect(findEmptyState().text()).toContain(s__('Runners|No results found'));
+ expect(findEmptyState().text()).toContain(s__('Runners|Edit your search and try again'));
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js
index 04627e2307b..6932b3b5197 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/runner/components/runner_projects_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -34,7 +34,7 @@ describe('RunnerProjects', () => {
let mockRunnerProjectsQuery;
const findHeading = () => wrapper.find('h3');
- const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading);
+ const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader);
const findRunnerAssignedItems = () => wrapper.findAllComponents(RunnerAssignedItem);
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 52bd51a974b..eb9f85a7d0f 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -16,6 +16,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
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';
@@ -48,6 +49,8 @@ import {
groupRunnersCountData,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
} from '../mock_data';
Vue.use(VueApollo);
@@ -75,6 +78,7 @@ describe('GroupRunnersApp', () => {
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
+ const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
const findRunnerRow = (id) => extendedWrapper(wrapper.findByTestId(`runner-row-${id}`));
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
@@ -103,6 +107,8 @@ describe('GroupRunnersApp', () => {
provide: {
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
},
});
};
@@ -388,8 +394,8 @@ describe('GroupRunnersApp', () => {
await waitForPromises();
});
- it('shows a message for no results', async () => {
- expect(wrapper.text()).toContain('No runners found');
+ it('shows an empty state', async () => {
+ expect(findRunnerListEmptyState().exists()).toBe(true);
});
});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 1c2333b552c..3368fc21544 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -19,7 +19,10 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runne
// Other mock data
export const onlineContactTimeoutSecs = 2 * 60 * 60;
-export const staleTimeoutSecs = 5259492; // Ruby's `2.months`
+export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
+
+export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
+export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
export {
runnersData,
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index a3c1458ed26..1f102f86b2a 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -5,6 +5,7 @@ import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
+ isSearchFiltered,
} from '~/runner/runner_search_utils';
describe('search_params.js', () => {
@@ -14,6 +15,7 @@ describe('search_params.js', () => {
urlQuery: '',
search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ isDefault: true,
},
{
name: 'a single status',
@@ -268,7 +270,7 @@ describe('search_params.js', () => {
describe('fromSearchToUrl', () => {
examples.forEach(({ name, urlQuery, search }) => {
it(`Converts ${name} to a url`, () => {
- expect(fromSearchToUrl(search)).toEqual(`http://test.host/${urlQuery}`);
+ expect(fromSearchToUrl(search)).toBe(`http://test.host/${urlQuery}`);
});
});
@@ -280,7 +282,7 @@ describe('search_params.js', () => {
const search = { filters: [], sort: 'CREATED_DESC' };
const expectedUrl = `http://test.host/`;
- expect(fromSearchToUrl(search, initalUrl)).toEqual(expectedUrl);
+ expect(fromSearchToUrl(search, initalUrl)).toBe(expectedUrl);
});
it('When unrelated search parameter is present, it does not get removed', () => {
@@ -288,7 +290,7 @@ describe('search_params.js', () => {
const search = { filters: [], sort: 'CREATED_DESC' };
const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
- expect(fromSearchToUrl(search, initialUrl)).toEqual(expectedUrl);
+ expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
});
});
@@ -331,4 +333,16 @@ describe('search_params.js', () => {
});
});
});
+
+ describe('isSearchFiltered', () => {
+ examples.forEach(({ name, search, isDefault }) => {
+ it(`Given ${name}, evaluates to ${isDefault ? 'not ' : ''}filtered`, () => {
+ expect(isSearchFiltered(search)).toBe(!isDefault);
+ });
+ });
+
+ it('given a missing pagination, evaluates as not filtered', () => {
+ expect(isSearchFiltered({ pagination: null })).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 67bd3194f20..2f93d3f6805 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -121,19 +121,12 @@ describe('Global Search Store Actions', () => {
describe('when groupId is set', () => {
it('calls Api.groupProjects with expected parameters', () => {
- const callbackTest = jest.fn();
- actions.fetchProjects({ commit: mockCommit, state }, undefined, callbackTest);
- expect(Api.groupProjects).toHaveBeenCalledWith(
- state.query.group_id,
- state.query.search,
- {
- order_by: 'similarity',
- include_subgroups: true,
- with_shared: false,
- },
- callbackTest,
- true,
- );
+ actions.fetchProjects({ commit: mockCommit, state }, undefined);
+ expect(Api.groupProjects).toHaveBeenCalledWith(state.query.group_id, state.query.search, {
+ order_by: 'similarity',
+ include_subgroups: true,
+ with_shared: false,
+ });
expect(Api.projects).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 4639552b4d3..266f047e9dc 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -53,7 +53,7 @@ describe('Search autocomplete dropdown', () => {
};
const disableProjectIssues = () => {
- document.querySelector('.js-search-project-options').setAttribute('data-issues-disabled', true);
+ document.querySelector('.js-search-project-options').dataset.issuesDisabled = true;
};
// Mock `gl` object in window for dashboard specific page. App code will need it.
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index d7d46d0d415..de91e51924d 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -2,7 +2,6 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlTab, GlTabs, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children';
@@ -20,22 +19,14 @@ import {
LICENSE_COMPLIANCE_DESCRIPTION,
LICENSE_COMPLIANCE_HELP_PATH,
AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
- LICENSE_ULTIMATE,
- LICENSE_PREMIUM,
- LICENSE_FREE,
} from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import currentLicenseQuery from '~/security_configuration/graphql/current_license.query.graphql';
-import waitForPromises from 'helpers/wait_for_promises';
-
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants';
-import { getCurrentLicensePlanResponse } from '../mock_data';
const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
@@ -50,31 +41,16 @@ Vue.use(VueApollo);
describe('App component', () => {
let wrapper;
let userCalloutDismissSpy;
- let mockApollo;
- const createComponent = ({
- shouldShowCallout = true,
- licenseQueryResponse = LICENSE_ULTIMATE,
- ...propsData
- }) => {
+ const createComponent = ({ shouldShowCallout = true, ...propsData }) => {
userCalloutDismissSpy = jest.fn();
- mockApollo = createMockApollo([
- [
- currentLicenseQuery,
- jest
- .fn()
- .mockResolvedValue(
- licenseQueryResponse instanceof Error
- ? licenseQueryResponse
- : getCurrentLicensePlanResponse(licenseQueryResponse),
- ),
- ],
- ]);
-
wrapper = extendedWrapper(
mount(SecurityConfigurationApp, {
- propsData,
+ propsData: {
+ securityTrainingEnabled: true,
+ ...propsData,
+ },
provide: {
upgradePath,
autoDevopsHelpPagePath,
@@ -82,7 +58,6 @@ describe('App component', () => {
projectFullPath,
vulnerabilityTrainingDocsPath,
},
- apolloProvider: mockApollo,
stubs: {
...stubChildren(SecurityConfigurationApp),
GlLink: false,
@@ -157,7 +132,6 @@ describe('App component', () => {
afterEach(() => {
wrapper.destroy();
- mockApollo = null;
});
describe('basic structure', () => {
@@ -166,7 +140,6 @@ describe('App component', () => {
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
});
- await waitForPromises();
});
it('renders main-heading with correct text', () => {
@@ -469,47 +442,42 @@ describe('App component', () => {
});
describe('Vulnerability management', () => {
- beforeEach(async () => {
+ it('does not show tab if security training is disabled', () => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
+ securityTrainingEnabled: false,
});
- await waitForPromises();
- });
- it('renders TrainingProviderList component', () => {
- expect(findTrainingProviderList().exists()).toBe(true);
+ expect(findVulnerabilityManagementTab().exists()).toBe(false);
});
- it('renders security training description', () => {
- expect(findVulnerabilityManagementTab().text()).toContain(i18n.securityTrainingDescription);
- });
-
- it('renders link to help docs', () => {
- const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
-
- expect(trainingLink.text()).toBe('Learn more about vulnerability training');
- expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
- });
-
- it.each`
- licenseQueryResponse | display
- ${LICENSE_ULTIMATE} | ${true}
- ${LICENSE_PREMIUM} | ${false}
- ${LICENSE_FREE} | ${false}
- ${null} | ${true}
- ${new Error()} | ${true}
- `(
- 'displays $display for license $licenseQueryResponse',
- async ({ licenseQueryResponse, display }) => {
+ describe('security training enabled', () => {
+ beforeEach(async () => {
createComponent({
- licenseQueryResponse,
augmentedSecurityFeatures: securityFeaturesMock,
augmentedComplianceFeatures: complianceFeaturesMock,
});
- await waitForPromises();
- expect(findVulnerabilityManagementTab().exists()).toBe(display);
- },
- );
+ });
+
+ it('shows the tab if security training is enabled', () => {
+ expect(findVulnerabilityManagementTab().exists()).toBe(true);
+ });
+
+ it('renders TrainingProviderList component', () => {
+ expect(findTrainingProviderList().exists()).toBe(true);
+ });
+
+ it('renders security training description', () => {
+ expect(findVulnerabilityManagementTab().text()).toContain(i18n.securityTrainingDescription);
+ });
+
+ it('renders link to help docs', () => {
+ const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
+
+ expect(trainingLink.text()).toBe('Learn more about vulnerability training');
+ expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
+ });
+ });
});
});
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 94a36472a1d..18a480bf082 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -111,12 +111,3 @@ export const tempProviderLogos = {
svg: `<svg>${[testProviderName[1]]}</svg>`,
},
};
-
-export const getCurrentLicensePlanResponse = (plan) => ({
- data: {
- currentLicense: {
- id: 'gid://gitlab/License/1',
- plan,
- },
- },
-});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 69f6a6e6e04..a286eeef14f 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -1,5 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
+import { TYPE_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import userDataMock from '../../user_data_mock';
@@ -32,6 +35,7 @@ describe('AssigneeAvatarLink component', () => {
});
const findTooltipText = () => wrapper.attributes('title');
+ const findUserLink = () => wrapper.findComponent(GlLink);
it('has the root url present in the assigneeUrl method', () => {
createComponent();
@@ -112,4 +116,24 @@ describe('AssigneeAvatarLink component', () => {
});
},
);
+
+ it('passes the correct user id for REST API', () => {
+ createComponent({
+ tooltipHasName: true,
+ user: userDataMock(),
+ });
+
+ expect(findUserLink().attributes('data-user-id')).toBe(String(userDataMock().id));
+ });
+
+ it('passes the correct user id for GraphQL API', () => {
+ const userId = userDataMock().id;
+
+ createComponent({
+ tooltipHasName: true,
+ user: { ...userDataMock(), id: convertToGraphQLId(TYPE_USER, userId) },
+ });
+
+ expect(findUserLink().attributes('data-user-id')).toBe(String(userId));
+ });
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
index c870bbecd76..724fba62479 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
@@ -72,7 +72,7 @@ describe('boards sidebar remove issue', () => {
createComponent({ canUpdate: true, slots });
findEditButton().vm.$emit('click');
- await nextTick;
+ await nextTick();
expect(findCollapsed().isVisible()).toBe(false);
expect(findExpanded().isVisible()).toBe(true);
diff --git a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
index 959fa799eb7..58fa878a189 100644
--- a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
+++ b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
@@ -41,18 +41,18 @@ describe('Attention require toggle', () => {
);
it.each`
- attentionRequested | variant
- ${true} | ${'warning'}
- ${false} | ${'default'}
+ attentionRequested | selected
+ ${true} | ${true}
+ ${false} | ${false}
`(
- 'renders button with variant $variant when attention_requested is $attentionRequested',
- ({ attentionRequested, variant }) => {
+ 'renders button with as selected when $selected when attention_requested is $attentionRequested',
+ ({ attentionRequested, selected }) => {
factory({
type: 'reviewer',
user: { attention_requested: attentionRequested, can_update_merge_request: true },
});
- expect(findToggle().props('variant')).toBe(variant);
+ expect(findToggle().props('selected')).toBe(selected);
},
);
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
index ab45fdf03bc..81354d64a90 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
@@ -69,14 +69,14 @@ describe('Sidebar Confidentiality Content', () => {
variant: 'warning',
});
expect(alertEl.text()).toBe(
- 'Only project members with at least Reporter role can view or be notified about this issue.',
+ 'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.',
);
});
it('displays a correct confidential text for epic', () => {
createComponent({ confidential: true, issuableType: 'epic' });
expect(findText().findComponent(GlAlert).text()).toBe(
- 'Only group members with at least Reporter role can view or be notified about this epic.',
+ 'Only group members with at least the Reporter role can view or be notified about this epic.',
);
});
});
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
index 85d6bc7b782..1ea035c7184 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -89,7 +89,7 @@ describe('Sidebar Confidentiality Form', () => {
it('renders a message about making an issue confidential', () => {
expect(findWarningMessage().text()).toBe(
- 'You are going to turn on confidentiality. Only project members with at least Reporter role can view or be notified about this issue.',
+ 'You are going to turn on confidentiality. Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.',
);
});
diff --git a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
index a8dc610672c..88a4913a27f 100644
--- a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
@@ -1,6 +1,12 @@
import { createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import {
+ fetchData,
+ fetchError,
+ mutationData,
+ mutationError,
+} from 'ee_else_ce_jest/sidebar/components/incidents/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -12,7 +18,6 @@ import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation
import { STATUS_ACKNOWLEDGED } from '~/sidebar/components/incidents/constants';
import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
-import { fetchData, fetchError, mutationData, mutationError } from './mock_data';
jest.mock('~/lib/logger');
jest.mock('~/flash');
diff --git a/spec/frontend/sidebar/components/time_tracking/mock_data.js b/spec/frontend/sidebar/components/time_tracking/mock_data.js
index ba2781118d9..f161ae677d0 100644
--- a/spec/frontend/sidebar/components/time_tracking/mock_data.js
+++ b/spec/frontend/sidebar/components/time_tracking/mock_data.js
@@ -1,3 +1,5 @@
+export const timelogToRemoveId = 'gid://gitlab/Timelog/18';
+
export const getIssueTimelogsQueryResponse = {
data: {
issuable: {
@@ -9,7 +11,7 @@ export const getIssueTimelogsQueryResponse = {
nodes: [
{
__typename: 'Timelog',
- id: 'gid://gitlab/Timelog/18',
+ id: timelogToRemoveId,
timeSpent: 14400,
user: {
id: 'user-1',
@@ -23,6 +25,10 @@ export const getIssueTimelogsQueryResponse = {
__typename: 'Note',
},
summary: 'A summary',
+ userPermissions: {
+ adminTimelog: true,
+ __typename: 'TimelogPermissions',
+ },
},
{
__typename: 'Timelog',
@@ -36,6 +42,10 @@ export const getIssueTimelogsQueryResponse = {
spentAt: '2021-05-07T13:19:01Z',
note: null,
summary: 'A summary',
+ userPermissions: {
+ adminTimelog: false,
+ __typename: 'TimelogPermissions',
+ },
},
{
__typename: 'Timelog',
@@ -53,6 +63,10 @@ export const getIssueTimelogsQueryResponse = {
__typename: 'Note',
},
summary: null,
+ userPermissions: {
+ adminTimelog: false,
+ __typename: 'TimelogPermissions',
+ },
},
],
__typename: 'TimelogConnection',
@@ -85,6 +99,10 @@ export const getMrTimelogsQueryResponse = {
__typename: 'Note',
},
summary: null,
+ userPermissions: {
+ adminTimelog: true,
+ __typename: 'TimelogPermissions',
+ },
},
{
__typename: 'Timelog',
@@ -98,6 +116,10 @@ export const getMrTimelogsQueryResponse = {
spentAt: '2021-05-07T14:44:39Z',
note: null,
summary: null,
+ userPermissions: {
+ adminTimelog: true,
+ __typename: 'TimelogPermissions',
+ },
},
{
__typename: 'Timelog',
@@ -115,6 +137,10 @@ export const getMrTimelogsQueryResponse = {
__typename: 'Note',
},
summary: null,
+ userPermissions: {
+ adminTimelog: true,
+ __typename: 'TimelogPermissions',
+ },
},
],
__typename: 'TimelogConnection',
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 2b17e6dd6c3..5ed8810e95e 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -1,15 +1,21 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { getAllByRole, getByRole } from '@testing-library/dom';
+import { getAllByRole, getByRole, getAllByTestId } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import Report from '~/sidebar/components/time_tracking/report.vue';
import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
-import { getIssueTimelogsQueryResponse, getMrTimelogsQueryResponse } from './mock_data';
+import deleteTimelogMutation from '~/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql';
+import {
+ getIssueTimelogsQueryResponse,
+ getMrTimelogsQueryResponse,
+ timelogToRemoveId,
+} from './mock_data';
jest.mock('~/flash');
@@ -18,6 +24,7 @@ describe('Issuable Time Tracking Report', () => {
let wrapper;
let fakeApollo;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findDeleteButton = () => wrapper.findByTestId('deleteButton');
const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse);
const successMrQueryHandler = jest.fn().mockResolvedValue(getMrTimelogsQueryResponse);
@@ -31,14 +38,16 @@ describe('Issuable Time Tracking Report', () => {
[getIssueTimelogsQuery, queryHandler],
[getMrTimelogsQuery, queryHandler],
]);
- wrapper = mountFunction(Report, {
- provide: {
- issuableId: 1,
- issuableType,
- },
- propsData: { limitToHours, issuableId: '1' },
- apolloProvider: fakeApollo,
- });
+ wrapper = extendedWrapper(
+ mountFunction(Report, {
+ provide: {
+ issuableId: 1,
+ issuableType,
+ },
+ propsData: { limitToHours, issuableId: '1' },
+ apolloProvider: fakeApollo,
+ }),
+ );
};
afterEach(() => {
@@ -75,6 +84,7 @@ describe('Issuable Time Tracking Report', () => {
expect(getAllByRole(wrapper.element, 'row', { name: /Administrator/i })).toHaveLength(2);
expect(getAllByRole(wrapper.element, 'row', { name: /A note/i })).toHaveLength(1);
expect(getAllByRole(wrapper.element, 'row', { name: /A summary/i })).toHaveLength(2);
+ expect(getAllByTestId(wrapper.element, 'deleteButton')).toHaveLength(1);
});
});
@@ -95,6 +105,7 @@ describe('Issuable Time Tracking Report', () => {
await waitForPromises();
expect(getAllByRole(wrapper.element, 'row', { name: /Administrator/i })).toHaveLength(3);
+ expect(getAllByTestId(wrapper.element, 'deleteButton')).toHaveLength(3);
});
});
@@ -123,4 +134,59 @@ describe('Issuable Time Tracking Report', () => {
});
});
});
+
+ describe('when clicking on the delete timelog button', () => {
+ beforeEach(() => {
+ mountComponent({ mountFunction: mount });
+ });
+
+ it('calls `$apollo.mutate` with deleteTimelogMutation mutation and removes the row', async () => {
+ const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: {
+ timelogDelete: {
+ errors: [],
+ },
+ },
+ });
+
+ await waitForPromises();
+ await findDeleteButton().trigger('click');
+ await waitForPromises();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(mutateSpy).toHaveBeenCalledWith({
+ mutation: deleteTimelogMutation,
+ variables: {
+ input: {
+ id: timelogToRemoveId,
+ },
+ },
+ update: expect.anything(),
+ });
+ });
+
+ it('calls `createFlash` with errorMessage and does not remove the row on promise reject', async () => {
+ const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
+
+ await waitForPromises();
+ await findDeleteButton().trigger('click');
+ await waitForPromises();
+
+ expect(mutateSpy).toHaveBeenCalledWith({
+ mutation: deleteTimelogMutation,
+ variables: {
+ input: {
+ id: timelogToRemoveId,
+ },
+ },
+ update: expect.anything(),
+ });
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred while removing the timelog.',
+ captureError: true,
+ error: expect.any(Object),
+ });
+ });
+ });
});
diff --git a/spec/frontend/static_site_editor/components/app_spec.js b/spec/frontend/static_site_editor/components/app_spec.js
deleted file mode 100644
index bbdffeae68f..00000000000
--- a/spec/frontend/static_site_editor/components/app_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import App from '~/static_site_editor/components/app.vue';
-
-describe('static_site_editor/components/app', () => {
- const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
- const RouterView = {
- template: '<div></div>',
- };
- let wrapper;
-
- const buildWrapper = () => {
- wrapper = shallowMount(App, {
- stubs: {
- RouterView,
- },
- propsData: {
- mergeRequestsIllustrationPath,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('passes merge request illustration path to the router view component', () => {
- buildWrapper();
-
- expect(wrapper.find(RouterView).attributes()).toMatchObject({
- 'merge-requests-illustration-path': mergeRequestsIllustrationPath,
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
deleted file mode 100644
index a833fd9ff9e..00000000000
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ /dev/null
@@ -1,264 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { stubComponent } from 'helpers/stub_component';
-
-import EditArea from '~/static_site_editor/components/edit_area.vue';
-import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
-import EditHeader from '~/static_site_editor/components/edit_header.vue';
-import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
-import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
-import { EDITOR_TYPES } from '~/static_site_editor/rich_content_editor/constants';
-import RichContentEditor from '~/static_site_editor/rich_content_editor/rich_content_editor.vue';
-
-import {
- sourceContentTitle as title,
- sourceContentYAML as content,
- sourceContentHeaderObjYAML as headerSettings,
- sourceContentBody as body,
- returnUrl,
- mounts,
- project,
- branch,
- baseUrl,
- imageRoot,
-} from '../mock_data';
-
-jest.mock('~/static_site_editor/services/formatter', () => jest.fn((str) => `${str} format-pass`));
-
-describe('~/static_site_editor/components/edit_area.vue', () => {
- let wrapper;
- const formattedBody = `${body} format-pass`;
- const savingChanges = true;
- const newBody = `new ${body}`;
-
- const RichContentEditorStub = stubComponent(RichContentEditor, {
- methods: {
- resetInitialValue: jest.fn(),
- },
- });
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(EditArea, {
- propsData: {
- title,
- content,
- returnUrl,
- mounts,
- project,
- branch,
- baseUrl,
- imageRoot,
- savingChanges,
- ...propsData,
- },
- stubs: { RichContentEditor: RichContentEditorStub },
- });
- };
-
- const findEditHeader = () => wrapper.find(EditHeader);
- const findEditDrawer = () => wrapper.find(EditDrawer);
- const findRichContentEditor = () => wrapper.find(RichContentEditor);
- const findPublishToolbar = () => wrapper.find(PublishToolbar);
- const findUnsavedChangesConfirmDialog = () => wrapper.find(UnsavedChangesConfirmDialog);
-
- beforeEach(() => {
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders edit header', () => {
- expect(findEditHeader().exists()).toBe(true);
- expect(findEditHeader().props('title')).toBe(title);
- });
-
- it('renders edit drawer', () => {
- expect(findEditDrawer().exists()).toBe(true);
- });
-
- it('renders rich content editor with a format pass', () => {
- expect(findRichContentEditor().exists()).toBe(true);
- expect(findRichContentEditor().props('content')).toBe(formattedBody);
- });
-
- it('renders publish toolbar', () => {
- expect(findPublishToolbar().exists()).toBe(true);
- expect(findPublishToolbar().props()).toMatchObject({
- returnUrl,
- savingChanges,
- saveable: false,
- });
- });
-
- it('renders unsaved changes confirm dialog', () => {
- expect(findUnsavedChangesConfirmDialog().exists()).toBe(true);
- expect(findUnsavedChangesConfirmDialog().props('modified')).toBe(false);
- });
-
- describe('when content changes', () => {
- beforeEach(() => {
- findRichContentEditor().vm.$emit('input', newBody);
-
- return nextTick();
- });
-
- it('updates parsedSource with new content', () => {
- const newContent = 'New content';
- const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncContent');
-
- findRichContentEditor().vm.$emit('input', newContent);
-
- expect(spySyncParsedSource).toHaveBeenCalledWith(newContent, true);
- });
-
- it('sets publish toolbar as saveable', () => {
- expect(findPublishToolbar().props('saveable')).toBe(true);
- });
-
- it('sets unsaved changes confirm dialog as modified', () => {
- expect(findUnsavedChangesConfirmDialog().props('modified')).toBe(true);
- });
-
- it('sets publish toolbar as not saveable when content changes are rollback', async () => {
- findRichContentEditor().vm.$emit('input', formattedBody);
-
- await nextTick();
- expect(findPublishToolbar().props('saveable')).toBe(false);
- });
- });
-
- describe('when the mode changes', () => {
- const setInitialMode = (mode) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ editorMode: mode });
- };
-
- afterEach(() => {
- setInitialMode(EDITOR_TYPES.wysiwyg);
- });
-
- it.each`
- initialMode | targetMode | resetValue
- ${EDITOR_TYPES.wysiwyg} | ${EDITOR_TYPES.markdown} | ${`${content} format-pass format-pass`}
- ${EDITOR_TYPES.markdown} | ${EDITOR_TYPES.wysiwyg} | ${`${body} format-pass format-pass`}
- `(
- 'sets editorMode from $initialMode to $targetMode',
- ({ initialMode, targetMode, resetValue }) => {
- setInitialMode(initialMode);
-
- findRichContentEditor().vm.$emit('modeChange', targetMode);
-
- expect(RichContentEditorStub.methods.resetInitialValue).toHaveBeenCalledWith(resetValue);
- expect(wrapper.vm.editorMode).toBe(targetMode);
- },
- );
-
- it('should format the content', () => {
- findRichContentEditor().vm.$emit('modeChange', EDITOR_TYPES.markdown);
-
- expect(RichContentEditorStub.methods.resetInitialValue).toHaveBeenCalledWith(
- `${content} format-pass format-pass`,
- );
- });
- });
-
- describe('when content has front matter', () => {
- it('renders a closed edit drawer', () => {
- expect(findEditDrawer().exists()).toBe(true);
- expect(findEditDrawer().props('isOpen')).toBe(false);
- });
-
- it('opens the edit drawer', async () => {
- findPublishToolbar().vm.$emit('editSettings');
-
- await nextTick();
- expect(findEditDrawer().props('isOpen')).toBe(true);
- });
-
- it('closes the edit drawer', async () => {
- findEditDrawer().vm.$emit('close');
-
- await nextTick();
- expect(findEditDrawer().props('isOpen')).toBe(false);
- });
-
- it('forwards the matter settings when the drawer is open', async () => {
- findPublishToolbar().vm.$emit('editSettings');
-
- jest.spyOn(wrapper.vm.parsedSource, 'matter').mockReturnValueOnce(headerSettings);
-
- await nextTick();
- expect(findEditDrawer().props('settings')).toEqual(headerSettings);
- });
-
- it('enables toolbar submit button', () => {
- expect(findPublishToolbar().props('hasSettings')).toBe(true);
- });
-
- it('syncs matter changes regardless of edit mode', () => {
- const newSettings = { title: 'test' };
- const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncMatter');
-
- findEditDrawer().vm.$emit('updateSettings', newSettings);
-
- expect(spySyncParsedSource).toHaveBeenCalledWith(newSettings);
- });
-
- it('syncs matter changes to content in markdown mode', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ editorMode: EDITOR_TYPES.markdown });
-
- const newSettings = { title: 'test' };
-
- findEditDrawer().vm.$emit('updateSettings', newSettings);
-
- await nextTick();
- expect(findRichContentEditor().props('content')).toContain('title: test');
- });
- });
-
- describe('when content lacks front matter', () => {
- beforeEach(() => {
- buildWrapper({ content: body });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('does not render edit drawer', () => {
- expect(findEditDrawer().exists()).toBe(false);
- });
-
- it('does not enable toolbar submit button', () => {
- expect(findPublishToolbar().props('hasSettings')).toBe(false);
- });
- });
-
- describe('when content is submitted', () => {
- it('should format the content', () => {
- findPublishToolbar().vm.$emit('submit', content);
-
- expect(wrapper.emitted('submit')[0][0].content).toBe(`${content} format-pass format-pass`);
- expect(wrapper.emitted('submit').length).toBe(1);
- });
- });
-
- describe('when RichContentEditor component triggers load event', () => {
- it('stores formatted markdown provided in the event data', () => {
- const data = { formattedMarkdown: 'formatted markdown' };
-
- findRichContentEditor().vm.$emit('load', data);
-
- // We can access the formatted markdown when submitting changes
- findPublishToolbar().vm.$emit('submit');
-
- expect(wrapper.emitted('submit')[0][0]).toMatchObject(data);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/components/edit_drawer_spec.js b/spec/frontend/static_site_editor/components/edit_drawer_spec.js
deleted file mode 100644
index 402dfe441c5..00000000000
--- a/spec/frontend/static_site_editor/components/edit_drawer_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { GlDrawer } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-
-import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
-import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
-
-describe('~/static_site_editor/components/edit_drawer.vue', () => {
- let wrapper;
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(EditDrawer, {
- propsData: {
- isOpen: false,
- settings: { title: 'Some title' },
- ...propsData,
- },
- });
- };
-
- const findFrontMatterControls = () => wrapper.find(FrontMatterControls);
- const findGlDrawer = () => wrapper.find(GlDrawer);
-
- beforeEach(() => {
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders the GlDrawer', () => {
- expect(findGlDrawer().exists()).toBe(true);
- });
-
- it('renders the FrontMatterControls', () => {
- expect(findFrontMatterControls().exists()).toBe(true);
- });
-
- it('forwards the settings to FrontMatterControls', () => {
- expect(findFrontMatterControls().props('settings')).toBe(wrapper.props('settings'));
- });
-
- it('is closed by default', () => {
- expect(findGlDrawer().props('open')).toBe(false);
- });
-
- it('can open', () => {
- buildWrapper({ isOpen: true });
-
- expect(findGlDrawer().props('open')).toBe(true);
- });
-
- it.each`
- event | payload | finderFn
- ${'close'} | ${undefined} | ${findGlDrawer}
- ${'updateSettings'} | ${{ some: 'data' }} | ${findFrontMatterControls}
- `(
- 'forwards the emitted $event event from the $finderFn with $payload',
- ({ event, payload, finderFn }) => {
- finderFn().vm.$emit(event, payload);
-
- expect(wrapper.emitted(event)[0][0]).toBe(payload);
- expect(wrapper.emitted(event).length).toBe(1);
- },
- );
-});
diff --git a/spec/frontend/static_site_editor/components/edit_header_spec.js b/spec/frontend/static_site_editor/components/edit_header_spec.js
deleted file mode 100644
index 2b0fe226a0b..00000000000
--- a/spec/frontend/static_site_editor/components/edit_header_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import EditHeader from '~/static_site_editor/components/edit_header.vue';
-import { DEFAULT_HEADING } from '~/static_site_editor/constants';
-
-import { sourceContentTitle } from '../mock_data';
-
-describe('~/static_site_editor/components/edit_header.vue', () => {
- let wrapper;
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(EditHeader, {
- propsData: {
- ...propsData,
- },
- });
- };
-
- const findHeading = () => wrapper.find({ ref: 'sseHeading' });
-
- beforeEach(() => {
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the default heading if there is no title prop', () => {
- expect(findHeading().text()).toBe(DEFAULT_HEADING);
- });
-
- it('renders the title prop value in the heading', () => {
- buildWrapper({ title: sourceContentTitle });
-
- expect(findHeading().text()).toBe(sourceContentTitle);
- });
-});
diff --git a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js b/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
deleted file mode 100644
index f6b29e98e5f..00000000000
--- a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlFormInput, GlFormTextarea } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-
-import { nextTick } from 'vue';
-import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
-
-import { mergeRequestMeta, mergeRequestTemplates } from '../mock_data';
-
-describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
- let wrapper;
- let mockSelect;
- let mockGlFormInputTitleInstance;
- const { title, description } = mergeRequestMeta;
- const newTitle = 'New title';
- const newDescription = 'New description';
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(EditMetaControls, {
- propsData: {
- title,
- description,
- templates: mergeRequestTemplates,
- currentTemplate: null,
- ...propsData,
- },
- });
- };
-
- const buildMocks = () => {
- mockSelect = jest.fn();
- mockGlFormInputTitleInstance = { $el: { select: mockSelect } };
- wrapper.vm.$refs.title = mockGlFormInputTitleInstance;
- };
-
- const findGlFormInputTitle = () => wrapper.find(GlFormInput);
- const findGlDropdownDescriptionTemplate = () => wrapper.find(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
- const findDropdownItemByIndex = (index) => findAllDropdownItems().at(index);
-
- const findGlFormTextAreaDescription = () => wrapper.find(GlFormTextarea);
-
- beforeEach(async () => {
- buildWrapper();
- buildMocks();
-
- await nextTick();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders the title input', () => {
- expect(findGlFormInputTitle().exists()).toBe(true);
- });
-
- it('renders the description template dropdown', () => {
- expect(findGlDropdownDescriptionTemplate().exists()).toBe(true);
- });
-
- it('renders the description input', () => {
- expect(findGlFormTextAreaDescription().exists()).toBe(true);
- });
-
- it('forwards the title prop to the title input', () => {
- expect(findGlFormInputTitle().attributes().value).toBe(title);
- });
-
- it('forwards the description prop to the description input', () => {
- expect(findGlFormTextAreaDescription().attributes().value).toBe(description);
- });
-
- it('calls select on the title input when mounted', () => {
- expect(mockGlFormInputTitleInstance.$el.select).toHaveBeenCalled();
- });
-
- it('renders a GlDropdownItem per template plus one (for the starting none option)', () => {
- expect(findDropdownItemByIndex(0).text()).toBe('None');
- expect(findAllDropdownItems().length).toBe(mergeRequestTemplates.length + 1);
- });
-
- describe('when inputs change', () => {
- const storageKey = 'sse-merge-request-meta-local-storage-editable';
-
- afterEach(() => {
- localStorage.removeItem(storageKey);
- });
-
- it.each`
- findFn | key | value
- ${findGlFormInputTitle} | ${'title'} | ${newTitle}
- ${findGlFormTextAreaDescription} | ${'description'} | ${newDescription}
- `('emits updated settings when $findFn input updates', ({ key, value, findFn }) => {
- findFn().vm.$emit('input', value);
-
- const newSettings = { ...mergeRequestMeta, [key]: value };
-
- expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings);
- });
- });
-
- describe('when templates change', () => {
- it.each`
- index | value
- ${0} | ${null}
- ${1} | ${mergeRequestTemplates[0]}
- ${2} | ${mergeRequestTemplates[1]}
- `('emits a change template event when $index is clicked', ({ index, value }) => {
- findDropdownItemByIndex(index).vm.$emit('click');
-
- expect(wrapper.emitted('changeTemplate')[0][0]).toBe(value);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
deleted file mode 100644
index bf3f8b7f571..00000000000
--- a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
+++ /dev/null
@@ -1,172 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import axios from '~/lib/utils/axios_utils';
-import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
-import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
-import { MR_META_LOCAL_STORAGE_KEY } from '~/static_site_editor/constants';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import {
- sourcePath,
- mergeRequestMeta,
- mergeRequestTemplates,
- project as namespaceProject,
-} from '../mock_data';
-
-describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
- useLocalStorageSpy();
-
- let wrapper;
- let mockAxios;
- const { title, description } = mergeRequestMeta;
- const [namespace, project] = namespaceProject.split('/');
-
- const buildWrapper = (propsData = {}, data = {}) => {
- wrapper = shallowMount(EditMetaModal, {
- propsData: {
- sourcePath,
- namespace,
- project,
- ...propsData,
- },
- data: () => data,
- });
- };
-
- const buildMockAxios = () => {
- mockAxios = new MockAdapter(axios);
- const templatesMergeRequestsPath = `templates/merge_request`;
- mockAxios
- .onGet(`${namespace}/${project}/${templatesMergeRequestsPath}`)
- .reply(200, mergeRequestTemplates);
- };
-
- const buildMockRefs = () => {
- wrapper.vm.$refs.editMetaControls = { resetCachedEditable: jest.fn() };
- };
-
- const findGlModal = () => wrapper.find(GlModal);
- const findEditMetaControls = () => wrapper.find(EditMetaControls);
- const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
-
- beforeEach(async () => {
- localStorage.setItem(MR_META_LOCAL_STORAGE_KEY);
-
- buildMockAxios();
- buildWrapper();
- buildMockRefs();
-
- await nextTick();
- });
-
- afterEach(() => {
- mockAxios.restore();
-
- wrapper.destroy();
- wrapper = null;
- });
-
- it('initializes initial merge request meta with local storage data', async () => {
- const localStorageMeta = {
- title: 'stored title',
- description: 'stored description',
- templates: null,
- currentTemplate: null,
- };
-
- findLocalStorageSync().vm.$emit('input', localStorageMeta);
-
- await nextTick();
-
- expect(findEditMetaControls().props()).toEqual(localStorageMeta);
- });
-
- it('renders the modal', () => {
- expect(findGlModal().exists()).toBe(true);
- });
-
- it('renders the edit meta controls', () => {
- expect(findEditMetaControls().exists()).toBe(true);
- });
-
- it('contains the sourcePath in the title', () => {
- expect(findEditMetaControls().props('title')).toContain(sourcePath);
- });
-
- it('forwards the title prop', () => {
- expect(findEditMetaControls().props('title')).toBe(title);
- });
-
- it('forwards the description prop', () => {
- expect(findEditMetaControls().props('description')).toBe(description);
- });
-
- it('forwards the templates prop', () => {
- expect(findEditMetaControls().props('templates')).toBe(null);
- });
-
- it('forwards the currentTemplate prop', () => {
- expect(findEditMetaControls().props('currentTemplate')).toBe(null);
- });
-
- describe('when save button is clicked', () => {
- beforeEach(() => {
- findGlModal().vm.$emit('primary', mergeRequestMeta);
- });
-
- it('removes merge request meta from local storage', () => {
- expect(findLocalStorageSync().props().clear).toBe(true);
- });
-
- it('emits the primary event with mergeRequestMeta', () => {
- expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]);
- });
- });
-
- describe('when templates exist', () => {
- const template1 = mergeRequestTemplates[0];
-
- beforeEach(() => {
- buildWrapper({}, { templates: mergeRequestTemplates, currentTemplate: null });
- });
-
- it('sets the currentTemplate on the changeTemplate event', async () => {
- findEditMetaControls().vm.$emit('changeTemplate', template1);
-
- await nextTick();
-
- expect(findEditMetaControls().props().currentTemplate).toBe(template1);
-
- findEditMetaControls().vm.$emit('changeTemplate', null);
-
- await nextTick();
-
- expect(findEditMetaControls().props().currentTemplate).toBe(null);
- });
-
- it('updates the description on the changeTemplate event', async () => {
- findEditMetaControls().vm.$emit('changeTemplate', template1);
-
- await nextTick();
-
- expect(findEditMetaControls().props().description).toEqual(template1.content);
- });
- });
-
- it('emits the hide event', () => {
- findGlModal().vm.$emit('hide');
- expect(wrapper.emitted('hide')).toEqual([[]]);
- });
-
- it('stores merge request meta changes in local storage when changes happen', async () => {
- const newMeta = { title: 'new title', description: 'new description' };
-
- findEditMetaControls().vm.$emit('updateSettings', newMeta);
-
- await nextTick();
-
- expect(findLocalStorageSync().props('value')).toEqual(newMeta);
- });
-});
diff --git a/spec/frontend/static_site_editor/components/front_matter_controls_spec.js b/spec/frontend/static_site_editor/components/front_matter_controls_spec.js
deleted file mode 100644
index 5fda3b40306..00000000000
--- a/spec/frontend/static_site_editor/components/front_matter_controls_spec.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { GlFormGroup } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-
-import { humanize } from '~/lib/utils/text_utility';
-
-import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
-
-import { sourceContentHeaderObjYAML as settings } from '../mock_data';
-
-describe('~/static_site_editor/components/front_matter_controls.vue', () => {
- let wrapper;
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(FrontMatterControls, {
- propsData: {
- settings,
- ...propsData,
- },
- });
- };
-
- beforeEach(() => {
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render only the supported GlFormGroup types', () => {
- expect(wrapper.findAll(GlFormGroup)).toHaveLength(3);
- });
-
- it.each`
- key
- ${'layout'}
- ${'title'}
- ${'twitter_image'}
- `('renders field when key is $key', ({ key }) => {
- const glFormGroup = wrapper.find(`#sse-front-matter-form-group-${key}`);
- const glFormInput = wrapper.find(`#sse-front-matter-control-${key}`);
-
- expect(glFormGroup.exists()).toBe(true);
- expect(glFormGroup.attributes().label).toBe(humanize(key));
-
- expect(glFormInput.exists()).toBe(true);
- expect(glFormInput.attributes().value).toBe(settings[key]);
- });
-
- it.each`
- key
- ${'suppress_header'}
- ${'extra_css'}
- `('does not render field when key is $key', ({ key }) => {
- const glFormInput = wrapper.find(`#sse-front-matter-control-${key}`);
-
- expect(glFormInput.exists()).toBe(false);
- });
-
- it('emits updated settings when nested control updates', () => {
- const elId = `#sse-front-matter-control-title`;
- const glFormInput = wrapper.find(elId);
- const newTitle = 'New title';
-
- glFormInput.vm.$emit('input', newTitle);
-
- const newSettings = { ...settings, title: newTitle };
-
- expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings);
- });
-});
diff --git a/spec/frontend/static_site_editor/components/invalid_content_message_spec.js b/spec/frontend/static_site_editor/components/invalid_content_message_spec.js
deleted file mode 100644
index 7e699e9451c..00000000000
--- a/spec/frontend/static_site_editor/components/invalid_content_message_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
-
-describe('~/static_site_editor/components/invalid_content_message.vue', () => {
- let wrapper;
- const findDocumentationButton = () => wrapper.find({ ref: 'documentationButton' });
- const documentationUrl =
- 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman';
-
- beforeEach(() => {
- wrapper = shallowMount(InvalidContentMessage);
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the configuration button link', () => {
- expect(findDocumentationButton().exists()).toBe(true);
- expect(findDocumentationButton().attributes('href')).toBe(documentationUrl);
- });
-});
diff --git a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
deleted file mode 100644
index 9ba7e4a94d1..00000000000
--- a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
-
-import { returnUrl } from '../mock_data';
-
-describe('Static Site Editor Toolbar', () => {
- let wrapper;
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(PublishToolbar, {
- propsData: {
- hasSettings: false,
- saveable: false,
- ...propsData,
- },
- });
- };
-
- const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' });
- const findSaveChangesButton = () => wrapper.find({ ref: 'submit' });
- const findEditSettingsButton = () => wrapper.find({ ref: 'settings' });
-
- beforeEach(() => {
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('does not render Settings button', () => {
- expect(findEditSettingsButton().exists()).toBe(false);
- });
-
- it('renders Submit Changes button', () => {
- expect(findSaveChangesButton().exists()).toBe(true);
- });
-
- it('disables Submit Changes button', () => {
- expect(findSaveChangesButton().attributes('disabled')).toBe('true');
- });
-
- it('does not render the Submit Changes button with a loader', () => {
- expect(findSaveChangesButton().props('loading')).toBe(false);
- });
-
- it('does not render returnUrl link', () => {
- expect(findReturnUrlLink().exists()).toBe(false);
- });
-
- it('renders returnUrl link when returnUrl prop exists', () => {
- buildWrapper({ returnUrl });
-
- expect(findReturnUrlLink().exists()).toBe(true);
- expect(findReturnUrlLink().attributes('href')).toBe(returnUrl);
- });
-
- describe('when providing settings CTA', () => {
- it('enables Submit Changes button', () => {
- buildWrapper({ hasSettings: true });
-
- expect(findEditSettingsButton().exists()).toBe(true);
- });
- });
-
- describe('when saveable', () => {
- it('enables Submit Changes button', () => {
- buildWrapper({ saveable: true });
-
- expect(findSaveChangesButton().attributes('disabled')).toBeFalsy();
- });
- });
-
- describe('when saving changes', () => {
- beforeEach(() => {
- buildWrapper({ savingChanges: true });
- });
-
- it('renders the Submit Changes button with a loading indicator', () => {
- expect(findSaveChangesButton().props('loading')).toBe(true);
- });
- });
-
- it('emits submit event when submit button is clicked', () => {
- buildWrapper({ saveable: true });
-
- findSaveChangesButton().vm.$emit('click');
-
- expect(wrapper.emitted('submit')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
deleted file mode 100644
index 82a5c5f624a..00000000000
--- a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlButton, GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-
-import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
-
-import { submitChangesError as error } from '../mock_data';
-
-describe('Submit Changes Error', () => {
- let wrapper;
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(SubmitChangesError, {
- propsData: {
- ...propsData,
- },
- stubs: {
- GlAlert,
- },
- });
- };
-
- const findRetryButton = () => wrapper.find(GlButton);
- const findAlert = () => wrapper.find(GlAlert);
-
- beforeEach(() => {
- buildWrapper({ error });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders error message', () => {
- expect(findAlert().text()).toContain(error);
- });
-
- it('emits dismiss event when alert emits dismiss event', () => {
- findAlert().vm.$emit('dismiss');
-
- expect(wrapper.emitted('dismiss')).toHaveLength(1);
- });
-
- it('emits retry event when retry button is clicked', () => {
- findRetryButton().vm.$emit('click');
-
- expect(wrapper.emitted('retry')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/static_site_editor/components/unsaved_changes_confirm_dialog_spec.js b/spec/frontend/static_site_editor/components/unsaved_changes_confirm_dialog_spec.js
deleted file mode 100644
index 9b8b22da693..00000000000
--- a/spec/frontend/static_site_editor/components/unsaved_changes_confirm_dialog_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
-
-describe('static_site_editor/components/unsaved_changes_confirm_dialog', () => {
- let wrapper;
- let event;
- let returnValueSetter;
-
- const buildWrapper = (propsData = {}) => {
- wrapper = shallowMount(UnsavedChangesConfirmDialog, {
- propsData,
- });
- };
-
- beforeEach(() => {
- event = new Event('beforeunload');
-
- jest.spyOn(event, 'preventDefault');
- returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
- });
-
- afterEach(() => {
- event.preventDefault.mockRestore();
- returnValueSetter.mockRestore();
- wrapper.destroy();
- });
-
- it('displays confirmation dialog when modified = true', () => {
- buildWrapper({ modified: true });
- window.dispatchEvent(event);
-
- expect(event.preventDefault).toHaveBeenCalled();
- expect(returnValueSetter).toHaveBeenCalledWith('');
- });
-
- it('does not display confirmation dialog when modified = false', () => {
- buildWrapper();
- window.dispatchEvent(event);
-
- expect(event.preventDefault).not.toHaveBeenCalled();
- expect(returnValueSetter).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js
deleted file mode 100644
index 83ad23f7dcf..00000000000
--- a/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import fileResolver from '~/static_site_editor/graphql/resolvers/file';
-import loadSourceContent from '~/static_site_editor/services/load_source_content';
-
-import {
- projectId,
- sourcePath,
- sourceContentTitle as title,
- sourceContentYAML as content,
-} from '../../mock_data';
-
-jest.mock('~/static_site_editor/services/load_source_content', () => jest.fn());
-
-describe('static_site_editor/graphql/resolvers/file', () => {
- it('returns file content and title when fetching file successfully', () => {
- loadSourceContent.mockResolvedValueOnce({ title, content });
-
- return fileResolver({ fullPath: projectId }, { path: sourcePath }).then((file) => {
- expect(file).toEqual({
- __typename: 'File',
- title,
- content,
- });
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/has_submitted_changes_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/has_submitted_changes_spec.js
deleted file mode 100644
index 0670b240a3f..00000000000
--- a/spec/frontend/static_site_editor/graphql/resolvers/has_submitted_changes_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import appDataQuery from '~/static_site_editor/graphql/queries/app_data.query.graphql';
-import hasSubmittedChanges from '~/static_site_editor/graphql/resolvers/has_submitted_changes';
-
-describe('static_site_editor/graphql/resolvers/has_submitted_changes', () => {
- it('updates the cache with the data passed in input', () => {
- const cachedData = { appData: { original: 'foo' } };
- const newValue = { input: { hasSubmittedChanges: true } };
-
- const cache = {
- readQuery: jest.fn().mockReturnValue(cachedData),
- writeQuery: jest.fn(),
- };
- hasSubmittedChanges(null, newValue, { cache });
-
- expect(cache.readQuery).toHaveBeenCalledWith({ query: appDataQuery });
- expect(cache.writeQuery).toHaveBeenCalledWith({
- query: appDataQuery,
- data: {
- appData: {
- __typename: 'AppData',
- original: 'foo',
- hasSubmittedChanges: true,
- },
- },
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
deleted file mode 100644
index a0529f5f945..00000000000
--- a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import savedContentMetaQuery from '~/static_site_editor/graphql/queries/saved_content_meta.query.graphql';
-import submitContentChangesResolver from '~/static_site_editor/graphql/resolvers/submit_content_changes';
-import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
-
-import {
- projectId as project,
- sourcePath,
- username,
- sourceContentYAML as content,
- savedContentMeta,
-} from '../../mock_data';
-
-jest.mock('~/static_site_editor/services/submit_content_changes', () => jest.fn());
-
-describe('static_site_editor/graphql/resolvers/submit_content_changes', () => {
- it('writes savedContentMeta query with the data returned by the submitContentChanges service', () => {
- const cache = { writeQuery: jest.fn() };
-
- submitContentChanges.mockResolvedValueOnce(savedContentMeta);
-
- return submitContentChangesResolver(
- {},
- { input: { path: sourcePath, project, sourcePath, content, username } },
- { cache },
- ).then(() => {
- expect(cache.writeQuery).toHaveBeenCalledWith({
- query: savedContentMetaQuery,
- data: {
- savedContentMeta: {
- __typename: 'SavedContentMeta',
- ...savedContentMeta,
- },
- },
- });
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js
deleted file mode 100644
index 8d64e1799b8..00000000000
--- a/spec/frontend/static_site_editor/mock_data.js
+++ /dev/null
@@ -1,91 +0,0 @@
-export const sourceContentHeaderYAML = `---
-layout: handbook-page-toc
-title: Handbook
-twitter_image: /images/tweets/handbook-gitlab.png
-suppress_header: true
-extra_css:
- - sales-and-free-trial-common.css
- - form-to-resource.css
----`;
-export const sourceContentHeaderObjYAML = {
- layout: 'handbook-page-toc',
- title: 'Handbook',
- twitter_image: '/images/tweets/handbook-gitlab.png',
- suppress_header: true,
- extra_css: ['sales-and-free-trial-common.css', 'form-to-resource.css'],
-};
-export const sourceContentSpacing = `\n`;
-export const sourceContentBody = `## On this page
-{:.no_toc .hidden-md .hidden-lg}
-
-- TOC
-{:toc .hidden-md .hidden-lg}
-
-![image](path/to/image1.png)`;
-export const sourceContentYAML = `${sourceContentHeaderYAML}${sourceContentSpacing}${sourceContentBody}`;
-export const sourceContentTitle = 'Handbook';
-
-export const username = 'gitlabuser';
-export const projectId = '123456';
-export const project = 'user1/project1';
-export const returnUrl = 'https://www.gitlab.com';
-export const sourcePath = 'foobar.md.html';
-export const mergeRequestMeta = {
- title: `Update ${sourcePath} file`,
- description: 'Copy update',
-};
-export const savedContentMeta = {
- branch: {
- label: 'foobar',
- url: 'foobar/-/tree/foobar',
- },
- commit: {
- label: 'c1461b08',
- url: 'foobar/-/c1461b08',
- },
- mergeRequest: {
- label: '123',
- url: 'foobar/-/merge_requests/123',
- },
-};
-export const mergeRequestTemplates = [
- { key: 'Template1', name: 'Template 1', content: 'This is template 1!' },
- { key: 'Template2', name: 'Template 2', content: 'This is template 2!' },
-];
-
-export const submitChangesError = 'Could not save changes';
-export const commitBranchResponse = {
- web_url: '/tree/root-main-patch-88195',
-};
-export const commitMultipleResponse = {
- short_id: 'ed899a2f4b5',
- web_url: '/commit/ed899a2f4b5',
-};
-export const createMergeRequestResponse = {
- iid: '123',
- web_url: '/merge_requests/123',
-};
-
-export const trackingCategory = 'projects:static_site_editor:show';
-
-export const images = new Map([
- ['path/to/image1.png', 'image1-content'],
- ['path/to/image2.png', 'image2-content'],
-]);
-
-export const mounts = [
- {
- source: 'default/source/',
- target: '',
- },
- {
- source: 'source/with/target',
- target: 'target',
- },
-];
-
-export const branch = 'main';
-
-export const baseUrl = '/user1/project1/-/sse/main%2Ftest.md';
-
-export const imageRoot = 'source/images/';
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
deleted file mode 100644
index 6571d295c36..00000000000
--- a/spec/frontend/static_site_editor/pages/home_spec.js
+++ /dev/null
@@ -1,301 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import EditArea from '~/static_site_editor/components/edit_area.vue';
-import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
-import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
-import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
-import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
-import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constants';
-import hasSubmittedChangesMutation from '~/static_site_editor/graphql/mutations/has_submitted_changes.mutation.graphql';
-import submitContentChangesMutation from '~/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql';
-import Home from '~/static_site_editor/pages/home.vue';
-import { SUCCESS_ROUTE } from '~/static_site_editor/router/constants';
-
-import {
- project,
- returnUrl,
- sourceContentYAML as content,
- sourceContentTitle as title,
- sourcePath,
- username,
- mergeRequestMeta,
- savedContentMeta,
- submitChangesError,
- trackingCategory,
- images,
- mounts,
- branch,
- baseUrl,
- imageRoot,
-} from '../mock_data';
-
-describe('static_site_editor/pages/home', () => {
- let wrapper;
- let store;
- let $apollo;
- let $router;
- let mutateMock;
- let trackingSpy;
- const defaultAppData = {
- isSupportedContent: true,
- hasSubmittedChanges: false,
- returnUrl,
- project,
- username,
- sourcePath,
- mounts,
- branch,
- baseUrl,
- imageUploadPath: imageRoot,
- };
- const hasSubmittedChangesMutationPayload = {
- data: {
- appData: { ...defaultAppData, hasSubmittedChanges: true },
- },
- };
-
- const buildApollo = (queries = {}) => {
- mutateMock = jest.fn();
-
- $apollo = {
- queries: {
- sourceContent: {
- loading: false,
- },
- ...queries,
- },
- mutate: mutateMock,
- };
- };
-
- const buildRouter = () => {
- $router = {
- push: jest.fn(),
- };
- };
-
- const buildWrapper = (data = {}) => {
- wrapper = shallowMount(Home, {
- store,
- mocks: {
- $apollo,
- $router,
- },
- data() {
- return {
- appData: { ...defaultAppData },
- sourceContent: { title, content },
- ...data,
- };
- },
- });
- };
-
- const findEditArea = () => wrapper.find(EditArea);
- const findEditMetaModal = () => wrapper.find(EditMetaModal);
- const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
- const findSkeletonLoader = () => wrapper.find(SkeletonLoader);
- const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
-
- beforeEach(() => {
- buildApollo();
- buildRouter();
-
- document.body.dataset.page = trackingCategory;
- trackingSpy = mockTracking(document.body.dataset.page, undefined, jest.spyOn);
- });
-
- afterEach(() => {
- wrapper.destroy();
- unmockTracking();
- wrapper = null;
- $apollo = null;
- });
-
- describe('when content is loaded', () => {
- beforeEach(() => {
- buildWrapper();
- });
-
- it('renders edit area', () => {
- expect(findEditArea().exists()).toBe(true);
- });
-
- it('provides source content, returnUrl, and isSavingChanges to the edit area', () => {
- expect(findEditArea().props()).toMatchObject({
- title,
- mounts,
- content,
- returnUrl,
- savingChanges: false,
- });
- });
- });
-
- it('does not render edit area when content is not loaded', () => {
- buildWrapper({ sourceContent: null });
-
- expect(findEditArea().exists()).toBe(false);
- });
-
- it('renders skeleton loader when content is not loading', () => {
- buildApollo({
- sourceContent: {
- loading: true,
- },
- });
- buildWrapper();
-
- expect(findSkeletonLoader().exists()).toBe(true);
- });
-
- it('does not render skeleton loader when content is not loading', () => {
- buildApollo({
- sourceContent: {
- loading: false,
- },
- });
- buildWrapper();
-
- expect(findSkeletonLoader().exists()).toBe(false);
- });
-
- it('displays invalid content message when content is not supported', () => {
- buildWrapper({ appData: { ...defaultAppData, isSupportedContent: false } });
-
- expect(findInvalidContentMessage().exists()).toBe(true);
- });
-
- it('does not display invalid content message when content is supported', () => {
- buildWrapper();
-
- expect(findInvalidContentMessage().exists()).toBe(false);
- });
-
- it('renders an EditMetaModal component', () => {
- buildWrapper();
-
- expect(findEditMetaModal().exists()).toBe(true);
- });
-
- describe('when preparing submission', () => {
- it('calls the show method when the edit-area submit event is emitted', async () => {
- buildWrapper();
-
- const mockInstance = { show: jest.fn() };
- wrapper.vm.$refs.editMetaModal = mockInstance;
-
- findEditArea().vm.$emit('submit', { content });
-
- await nextTick();
- expect(mockInstance.show).toHaveBeenCalled();
- });
- });
-
- describe('when submitting changes fails', () => {
- const setupMutateMock = () => {
- mutateMock
- .mockResolvedValueOnce(hasSubmittedChangesMutationPayload)
- .mockRejectedValueOnce(new Error(submitChangesError));
- };
-
- beforeEach(async () => {
- setupMutateMock();
-
- buildWrapper({ content });
- findEditMetaModal().vm.$emit('primary', mergeRequestMeta);
-
- await nextTick();
- });
-
- it('displays submit changes error message', () => {
- expect(findSubmitChangesError().exists()).toBe(true);
- });
-
- it('retries submitting changes when retry button is clicked', () => {
- setupMutateMock();
-
- findSubmitChangesError().vm.$emit('retry');
-
- expect(mutateMock).toHaveBeenCalled();
- });
-
- it('hides submit changes error message when dismiss button is clicked', async () => {
- findSubmitChangesError().vm.$emit('dismiss');
-
- await nextTick();
- expect(findSubmitChangesError().exists()).toBe(false);
- });
- });
-
- describe('when submitting changes succeeds', () => {
- const newContent = `new ${content}`;
- const formattedMarkdown = `formatted ${content}`;
-
- beforeEach(async () => {
- mutateMock.mockResolvedValueOnce(hasSubmittedChangesMutationPayload).mockResolvedValueOnce({
- data: {
- submitContentChanges: savedContentMeta,
- },
- });
-
- buildWrapper();
-
- findEditMetaModal().vm.show = jest.fn();
-
- findEditArea().vm.$emit('submit', { content: newContent, images, formattedMarkdown });
-
- findEditMetaModal().vm.$emit('primary', mergeRequestMeta);
-
- await nextTick();
- });
-
- it('dispatches hasSubmittedChanges mutation', () => {
- expect(mutateMock).toHaveBeenNthCalledWith(1, {
- mutation: hasSubmittedChangesMutation,
- variables: {
- input: {
- hasSubmittedChanges: true,
- },
- },
- });
- });
-
- it('dispatches submitContentChanges mutation', () => {
- expect(mutateMock).toHaveBeenNthCalledWith(2, {
- mutation: submitContentChangesMutation,
- variables: {
- input: {
- content: newContent,
- formattedMarkdown,
- project,
- sourcePath,
- targetBranch: branch,
- username,
- images,
- mergeRequestMeta,
- },
- },
- });
- });
-
- it('transitions to the SUCCESS route', () => {
- expect($router.push).toHaveBeenCalledWith(SUCCESS_ROUTE);
- });
- });
-
- it('does not display submit changes error when an error does not exist', () => {
- buildWrapper();
-
- expect(findSubmitChangesError().exists()).toBe(false);
- });
-
- it('tracks when editor is initialized on the mounted lifecycle hook', () => {
- buildWrapper();
- expect(trackingSpy).toHaveBeenCalledWith(
- document.body.dataset.page,
- TRACKING_ACTION_INITIALIZE_EDITOR,
- );
- });
-});
diff --git a/spec/frontend/static_site_editor/pages/success_spec.js b/spec/frontend/static_site_editor/pages/success_spec.js
deleted file mode 100644
index fbdc2c435a0..00000000000
--- a/spec/frontend/static_site_editor/pages/success_spec.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Success from '~/static_site_editor/pages/success.vue';
-import { HOME_ROUTE } from '~/static_site_editor/router/constants';
-import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
-
-describe('~/static_site_editor/pages/success.vue', () => {
- const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
- let wrapper;
- let router;
-
- const buildRouter = () => {
- router = {
- push: jest.fn(),
- };
- };
-
- const buildWrapper = (data = {}, appData = {}) => {
- wrapper = shallowMount(Success, {
- mocks: {
- $router: router,
- },
- stubs: {
- GlButton,
- GlEmptyState,
- GlLoadingIcon,
- },
- propsData: {
- mergeRequestsIllustrationPath,
- },
- data() {
- return {
- savedContentMeta,
- appData: {
- returnUrl,
- sourcePath,
- hasSubmittedChanges: true,
- ...appData,
- },
- ...data,
- };
- },
- });
- };
-
- const findReturnUrlButton = () => wrapper.find(GlButton);
- const findEmptyState = () => wrapper.find(GlEmptyState);
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
-
- beforeEach(() => {
- buildRouter();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when savedContentMeta is valid', () => {
- it('renders empty state with a link to the created merge request', () => {
- buildWrapper();
-
- expect(findEmptyState().exists()).toBe(true);
- expect(findEmptyState().props()).toMatchObject({
- primaryButtonText: 'View merge request',
- primaryButtonLink: savedContentMeta.mergeRequest.url,
- title: 'Your merge request has been created',
- svgPath: mergeRequestsIllustrationPath,
- svgHeight: 146,
- });
- });
-
- it('displays merge request instructions in the empty state', () => {
- buildWrapper();
-
- expect(findEmptyState().text()).toContain(
- 'To see your changes live you will need to do the following things:',
- );
- expect(findEmptyState().text()).toContain('1. Add a clear title to describe the change.');
- expect(findEmptyState().text()).toContain(
- '2. Add a description to explain why the change is being made.',
- );
- expect(findEmptyState().text()).toContain(
- '3. Assign a person to review and accept the merge request.',
- );
- });
-
- it('displays return to site button', () => {
- buildWrapper();
-
- expect(findReturnUrlButton().text()).toBe('Return to site');
- expect(findReturnUrlButton().attributes().href).toBe(returnUrl);
- });
-
- it('displays source path', () => {
- buildWrapper();
-
- expect(wrapper.text()).toContain(`Update ${sourcePath} file`);
- });
- });
-
- describe('when savedContentMeta is invalid', () => {
- it('renders empty state with a loader', () => {
- buildWrapper({ savedContentMeta: null });
-
- expect(findEmptyState().exists()).toBe(true);
- expect(findEmptyState().props()).toMatchObject({
- title: 'Creating your merge request',
- svgPath: mergeRequestsIllustrationPath,
- });
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('displays helper info in the empty state', () => {
- buildWrapper({ savedContentMeta: null });
-
- expect(findEmptyState().text()).toContain(
- 'You can set an assignee to get your changes reviewed and deployed once your merge request is created',
- );
- expect(findEmptyState().text()).toContain(
- 'A link to view the merge request will appear once ready',
- );
- });
-
- it('redirects to the HOME route when content has not been submitted', () => {
- buildWrapper({ savedContentMeta: null }, { hasSubmittedChanges: false });
-
- expect(router.push).toHaveBeenCalledWith(HOME_ROUTE);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/editor_service_spec.js b/spec/frontend/static_site_editor/rich_content_editor/editor_service_spec.js
deleted file mode 100644
index cd0d09c085f..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/editor_service_spec.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import buildCustomRenderer from '~/static_site_editor/rich_content_editor/services/build_custom_renderer';
-import buildHTMLToMarkdownRenderer from '~/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer';
-import {
- generateToolbarItem,
- addCustomEventListener,
- removeCustomEventListener,
- registerHTMLToMarkdownRenderer,
- addImage,
- insertVideo,
- getMarkdown,
- getEditorOptions,
-} from '~/static_site_editor/rich_content_editor/services/editor_service';
-import sanitizeHTML from '~/static_site_editor/rich_content_editor/services/sanitize_html';
-
-jest.mock('~/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer');
-jest.mock('~/static_site_editor/rich_content_editor/services/build_custom_renderer');
-jest.mock('~/static_site_editor/rich_content_editor/services/sanitize_html');
-
-describe('Editor Service', () => {
- let mockInstance;
- let event;
- let handler;
- const parseHtml = (str) => {
- const wrapper = document.createElement('div');
- wrapper.innerHTML = str;
- return wrapper.firstChild;
- };
-
- beforeEach(() => {
- mockInstance = {
- eventManager: { addEventType: jest.fn(), removeEventHandler: jest.fn(), listen: jest.fn() },
- editor: {
- exec: jest.fn(),
- isWysiwygMode: jest.fn(),
- getSquire: jest.fn(),
- insertText: jest.fn(),
- },
- invoke: jest.fn(),
- toMarkOptions: {
- renderer: {
- constructor: {
- factory: jest.fn(),
- },
- },
- },
- };
- event = 'someCustomEvent';
- handler = jest.fn();
- });
-
- describe('generateToolbarItem', () => {
- const config = {
- icon: 'bold',
- command: 'some-command',
- tooltip: 'Some Tooltip',
- event: 'some-event',
- };
-
- const generatedItem = generateToolbarItem(config);
-
- it('generates the correct command', () => {
- expect(generatedItem.options.command).toBe(config.command);
- });
-
- it('generates the correct event', () => {
- expect(generatedItem.options.event).toBe(config.event);
- });
-
- it('generates a divider when isDivider is set to true', () => {
- const isDivider = true;
-
- expect(generateToolbarItem({ isDivider })).toBe('divider');
- });
- });
-
- describe('addCustomEventListener', () => {
- it('registers an event type on the instance and adds an event handler', () => {
- addCustomEventListener(mockInstance, event, handler);
-
- expect(mockInstance.eventManager.addEventType).toHaveBeenCalledWith(event);
- expect(mockInstance.eventManager.listen).toHaveBeenCalledWith(event, handler);
- });
- });
-
- describe('removeCustomEventListener', () => {
- it('removes an event handler from the instance', () => {
- removeCustomEventListener(mockInstance, event, handler);
-
- expect(mockInstance.eventManager.removeEventHandler).toHaveBeenCalledWith(event, handler);
- });
- });
-
- describe('addImage', () => {
- const file = new File([], 'some-file.jpg');
- const mockImage = { imageUrl: 'some/url.png', altText: 'some alt text' };
-
- it('calls the insertElement method on the squire instance when in WYSIWYG mode', () => {
- jest.spyOn(URL, 'createObjectURL');
- mockInstance.editor.isWysiwygMode.mockReturnValue(true);
- mockInstance.editor.getSquire.mockReturnValue({ insertElement: jest.fn() });
-
- addImage(mockInstance, mockImage, file);
-
- expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalled();
- expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(file);
- });
-
- it('calls the insertText method on the instance when in Markdown mode', () => {
- mockInstance.editor.isWysiwygMode.mockReturnValue(false);
- addImage(mockInstance, mockImage, file);
-
- expect(mockInstance.editor.insertText).toHaveBeenCalledWith('![some alt text](some/url.png)');
- });
- });
-
- describe('insertVideo', () => {
- const mockUrl = 'some/url';
- const htmlString = `<figure contenteditable="false" class="gl-relative gl-h-0 video_container"><iframe class="gl-absolute gl-top-0 gl-left-0 gl-w-full gl-h-full" width="560" height="315" frameborder="0" src="some/url"></iframe></figure>`;
- const mockInsertElement = jest.fn();
-
- beforeEach(() =>
- mockInstance.editor.getSquire.mockReturnValue({ insertElement: mockInsertElement }),
- );
-
- describe('WYSIWYG mode', () => {
- it('calls the insertElement method on the squire instance with an iFrame element', () => {
- mockInstance.editor.isWysiwygMode.mockReturnValue(true);
-
- insertVideo(mockInstance, mockUrl);
-
- expect(mockInstance.editor.getSquire().insertElement).toHaveBeenCalledWith(
- parseHtml(htmlString),
- );
- });
- });
-
- describe('Markdown mode', () => {
- it('calls the insertText method on the editor instance with the iFrame element HTML', () => {
- mockInstance.editor.isWysiwygMode.mockReturnValue(false);
-
- insertVideo(mockInstance, mockUrl);
-
- expect(mockInstance.editor.insertText).toHaveBeenCalledWith(htmlString);
- });
- });
- });
-
- describe('getMarkdown', () => {
- it('calls the invoke method on the instance', () => {
- getMarkdown(mockInstance);
-
- expect(mockInstance.invoke).toHaveBeenCalledWith('getMarkdown');
- });
- });
-
- describe('registerHTMLToMarkdownRenderer', () => {
- let baseRenderer;
- const htmlToMarkdownRenderer = {};
- const extendedRenderer = {};
-
- beforeEach(() => {
- baseRenderer = mockInstance.toMarkOptions.renderer;
- buildHTMLToMarkdownRenderer.mockReturnValueOnce(htmlToMarkdownRenderer);
- baseRenderer.constructor.factory.mockReturnValueOnce(extendedRenderer);
-
- registerHTMLToMarkdownRenderer(mockInstance);
- });
-
- it('builds a new instance of the HTML to Markdown renderer', () => {
- expect(buildHTMLToMarkdownRenderer).toHaveBeenCalledWith(baseRenderer);
- });
-
- it('extends base renderer with the HTML to Markdown renderer', () => {
- expect(baseRenderer.constructor.factory).toHaveBeenCalledWith(
- baseRenderer,
- htmlToMarkdownRenderer,
- );
- });
-
- it('replaces the default renderer with extended renderer', () => {
- expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer);
- });
- });
-
- describe('getEditorOptions', () => {
- const externalOptions = {
- customRenderers: {},
- };
- const renderer = {};
-
- beforeEach(() => {
- buildCustomRenderer.mockReturnValueOnce(renderer);
- });
-
- it('generates a configuration object with a custom HTML renderer and toolbarItems', () => {
- expect(getEditorOptions()).toHaveProp('customHTMLRenderer', renderer);
- expect(getEditorOptions()).toHaveProp('toolbarItems');
- });
-
- it('passes external renderers to the buildCustomRenderers function', () => {
- getEditorOptions(externalOptions);
- expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers);
- });
-
- it('uses the internal sanitizeHTML service for HTML sanitization', () => {
- const options = getEditorOptions();
- const html = '<div></div>';
-
- options.customHTMLSanitizer(html);
-
- expect(sanitizeHTML).toHaveBeenCalledWith(html);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
deleted file mode 100644
index c8c9f45618d..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { GlModal, GlTabs } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { IMAGE_TABS } from '~/static_site_editor/rich_content_editor/constants';
-import AddImageModal from '~/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue';
-import UploadImageTab from '~/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue';
-
-describe('Add Image Modal', () => {
- let wrapper;
- const propsData = { imageRoot: 'path/to/root/' };
-
- const findModal = () => wrapper.find(GlModal);
- const findTabs = () => wrapper.find(GlTabs);
- const findUploadImageTab = () => wrapper.find(UploadImageTab);
- const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
- const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
-
- beforeEach(() => {
- wrapper = shallowMount(AddImageModal, { propsData });
- });
-
- describe('when content is loaded', () => {
- it('renders a modal component', () => {
- expect(findModal().exists()).toBe(true);
- });
-
- it('renders a Tabs component', () => {
- expect(findTabs().exists()).toBe(true);
- });
-
- it('renders an upload image tab', () => {
- expect(findUploadImageTab().exists()).toBe(true);
- });
-
- it('renders an input to add an image URL', () => {
- expect(findUrlInput().exists()).toBe(true);
- });
-
- it('renders an input to add an image description', () => {
- expect(findDescriptionInput().exists()).toBe(true);
- });
- });
-
- describe('add image', () => {
- describe('Upload', () => {
- it('validates the file', () => {
- const preventDefault = jest.fn();
- const description = 'some description';
- const file = { name: 'some_file.png' };
-
- wrapper.vm.$refs.uploadImageTab = { validateFile: jest.fn() };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ file, description, tabIndex: IMAGE_TABS.UPLOAD_TAB });
-
- findModal().vm.$emit('ok', { preventDefault });
-
- expect(wrapper.vm.$refs.uploadImageTab.validateFile).toHaveBeenCalled();
- });
- });
-
- describe('URL', () => {
- it('emits an addImage event when a valid URL is specified', () => {
- const preventDefault = jest.fn();
- const mockImage = { imageUrl: '/some/valid/url.png', description: 'some description' };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ ...mockImage, tabIndex: IMAGE_TABS.URL_TAB });
-
- findModal().vm.$emit('ok', { preventDefault });
- expect(preventDefault).not.toHaveBeenCalled();
- expect(wrapper.emitted('addImage')).toEqual([
- [{ imageUrl: mockImage.imageUrl, altText: mockImage.description }],
- ]);
- });
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab_spec.js
deleted file mode 100644
index 11b73d58259..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import UploadImageTab from '~/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue';
-
-describe('Upload Image Tab', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallowMount(UploadImageTab);
- });
-
- afterEach(() => wrapper.destroy());
-
- const triggerInputEvent = (size) => {
- const file = { size, name: 'file-name.png' };
- const mockEvent = new Event('input');
-
- Object.defineProperty(mockEvent, 'target', { value: { files: [file] } });
-
- wrapper.find({ ref: 'fileInput' }).element.dispatchEvent(mockEvent);
-
- return file;
- };
-
- describe('onInput', () => {
- it.each`
- size | fileError
- ${2000000000} | ${'Maximum file size is 2MB. Please select a smaller file.'}
- ${200} | ${null}
- `('validates the file correctly', ({ size, fileError }) => {
- triggerInputEvent(size);
-
- expect(wrapper.vm.fileError).toBe(fileError);
- });
- });
-
- it('emits input event when file is valid', () => {
- const file = triggerInputEvent(200);
-
- expect(wrapper.emitted('input')).toEqual([[file]]);
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/modals/insert_video_modal_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/insert_video_modal_spec.js
deleted file mode 100644
index 392d31bf039..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/modals/insert_video_modal_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import InsertVideoModal from '~/static_site_editor/rich_content_editor/modals/insert_video_modal.vue';
-
-describe('Insert Video Modal', () => {
- let wrapper;
-
- const findModal = () => wrapper.find(GlModal);
- const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
-
- const triggerInsertVideo = (url) => {
- const preventDefault = jest.fn();
- findUrlInput().vm.$emit('input', url);
- findModal().vm.$emit('primary', { preventDefault });
- };
-
- beforeEach(() => {
- wrapper = shallowMount(InsertVideoModal);
- });
-
- afterEach(() => wrapper.destroy());
-
- describe('when content is loaded', () => {
- it('renders a modal component', () => {
- expect(findModal().exists()).toBe(true);
- });
-
- it('renders an input to add a URL', () => {
- expect(findUrlInput().exists()).toBe(true);
- });
- });
-
- describe('insert video', () => {
- it.each`
- url | emitted
- ${'https://www.youtube.com/embed/someId'} | ${[['https://www.youtube.com/embed/someId']]}
- ${'https://www.youtube.com/watch?v=1234'} | ${[['https://www.youtube.com/embed/1234']]}
- ${'::youtube.com/invalid/url'} | ${undefined}
- `('formats the url correctly', ({ url, emitted }) => {
- triggerInsertVideo(url);
- expect(wrapper.emitted('insertVideo')).toEqual(emitted);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_integration_spec.js b/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_integration_spec.js
deleted file mode 100644
index 6c02ec506c6..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_integration_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import Editor from '@toast-ui/editor';
-import buildMarkdownToHTMLRenderer from '~/static_site_editor/rich_content_editor/services/build_custom_renderer';
-import { registerHTMLToMarkdownRenderer } from '~/static_site_editor/rich_content_editor/services/editor_service';
-
-describe('static_site_editor/rich_content_editor', () => {
- let editor;
-
- const buildEditor = () => {
- editor = new Editor({
- el: document.body,
- customHTMLRenderer: buildMarkdownToHTMLRenderer(),
- });
-
- registerHTMLToMarkdownRenderer(editor);
- };
-
- beforeEach(() => {
- buildEditor();
- });
-
- describe('HTML to Markdown', () => {
- it('uses "-" character list marker in unordered lists', () => {
- editor.setHtml('<ul><li>List item 1</li><li>List item 2</li></ul>');
-
- const markdown = editor.getMarkdown();
-
- expect(markdown).toBe('- List item 1\n- List item 2');
- });
-
- it('does not increment the list marker in ordered lists', () => {
- editor.setHtml('<ol><li>List item 1</li><li>List item 2</li></ol>');
-
- const markdown = editor.getMarkdown();
-
- expect(markdown).toBe('1. List item 1\n1. List item 2');
- });
-
- it('indents lists using four spaces', () => {
- editor.setHtml('<ul><li>List item 1</li><ul><li>List item 2</li></ul></ul>');
-
- const markdown = editor.getMarkdown();
-
- expect(markdown).toBe('- List item 1\n - List item 2');
- });
-
- it('uses * for strong and _ for emphasis text', () => {
- editor.setHtml('<strong>strong text</strong><i>emphasis text</i>');
-
- const markdown = editor.getMarkdown();
-
- expect(markdown).toBe('**strong text**_emphasis text_');
- });
- });
-
- describe('Markdown to HTML', () => {
- it.each`
- input | output
- ${'markdown with _emphasized\ntext_'} | ${'<p>markdown with <em>emphasized text</em></p>\n'}
- ${'markdown with **strong\ntext**'} | ${'<p>markdown with <strong>strong text</strong></p>\n'}
- `(
- 'does not transform softbreaks inside (_) and strong (**) nodes into <br/> tags',
- ({ input, output }) => {
- editor.setMarkdown(input);
-
- expect(editor.getHtml()).toBe(output);
- },
- );
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_spec.js
deleted file mode 100644
index 3b0d2993a5d..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_spec.js
+++ /dev/null
@@ -1,222 +0,0 @@
-import { Editor, mockEditorApi } from '@toast-ui/vue-editor';
-import { shallowMount } from '@vue/test-utils';
-import {
- EDITOR_TYPES,
- EDITOR_HEIGHT,
- EDITOR_PREVIEW_STYLE,
- CUSTOM_EVENTS,
-} from '~/static_site_editor/rich_content_editor/constants';
-import AddImageModal from '~/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue';
-import InsertVideoModal from '~/static_site_editor/rich_content_editor/modals/insert_video_modal.vue';
-import RichContentEditor from '~/static_site_editor/rich_content_editor/rich_content_editor.vue';
-
-import {
- addCustomEventListener,
- removeCustomEventListener,
- addImage,
- insertVideo,
- registerHTMLToMarkdownRenderer,
- getEditorOptions,
- getMarkdown,
-} from '~/static_site_editor/rich_content_editor/services/editor_service';
-
-jest.mock('~/static_site_editor/rich_content_editor/services/editor_service', () => ({
- addCustomEventListener: jest.fn(),
- removeCustomEventListener: jest.fn(),
- addImage: jest.fn(),
- insertVideo: jest.fn(),
- registerHTMLToMarkdownRenderer: jest.fn(),
- getEditorOptions: jest.fn(),
- getMarkdown: jest.fn(),
-}));
-
-describe('Rich Content Editor', () => {
- let wrapper;
-
- const content = '## Some Markdown';
- const imageRoot = 'path/to/root/';
- const findEditor = () => wrapper.find({ ref: 'editor' });
- const findAddImageModal = () => wrapper.find(AddImageModal);
- const findInsertVideoModal = () => wrapper.find(InsertVideoModal);
-
- const buildWrapper = async () => {
- wrapper = shallowMount(RichContentEditor, {
- propsData: { content, imageRoot },
- stubs: {
- ToastEditor: Editor,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when content is loaded', () => {
- const editorOptions = {};
-
- beforeEach(() => {
- getEditorOptions.mockReturnValueOnce(editorOptions);
- buildWrapper();
- });
-
- it('renders an editor', () => {
- expect(findEditor().exists()).toBe(true);
- });
-
- it('renders the correct content', () => {
- expect(findEditor().props().initialValue).toBe(content);
- });
-
- it('provides options generated by the getEditorOptions service', () => {
- expect(findEditor().props().options).toBe(editorOptions);
- });
-
- it('has the correct preview style', () => {
- expect(findEditor().props().previewStyle).toBe(EDITOR_PREVIEW_STYLE);
- });
-
- it('has the correct initial edit type', () => {
- expect(findEditor().props().initialEditType).toBe(EDITOR_TYPES.wysiwyg);
- });
-
- it('has the correct height', () => {
- expect(findEditor().props().height).toBe(EDITOR_HEIGHT);
- });
- });
-
- describe('when content is changed', () => {
- beforeEach(() => {
- buildWrapper();
- });
-
- it('emits an input event with the changed content', () => {
- const changedMarkdown = '## Changed Markdown';
- getMarkdown.mockReturnValueOnce(changedMarkdown);
-
- findEditor().vm.$emit('change');
-
- expect(wrapper.emitted().input[0][0]).toBe(changedMarkdown);
- });
- });
-
- describe('when content is reset', () => {
- beforeEach(() => {
- buildWrapper();
- });
-
- it('should reset the content via setMarkdown', () => {
- const newContent = 'Just the body content excluding the front matter for example';
- const mockInstance = { invoke: jest.fn() };
- wrapper.vm.$refs.editor = mockInstance;
-
- wrapper.vm.resetInitialValue(newContent);
-
- expect(mockInstance.invoke).toHaveBeenCalledWith('setMarkdown', newContent);
- });
- });
-
- describe('when editor is loaded', () => {
- const formattedMarkdown = 'formatted markdown';
-
- beforeEach(() => {
- mockEditorApi.getMarkdown.mockReturnValueOnce(formattedMarkdown);
- buildWrapper();
- });
-
- afterEach(() => {
- mockEditorApi.getMarkdown.mockReset();
- });
-
- it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
- expect(addCustomEventListener).toHaveBeenCalledWith(
- wrapper.vm.editorApi,
- CUSTOM_EVENTS.openAddImageModal,
- wrapper.vm.onOpenAddImageModal,
- );
- });
-
- it('adds the CUSTOM_EVENTS.openInsertVideoModal custom event listener', () => {
- expect(addCustomEventListener).toHaveBeenCalledWith(
- wrapper.vm.editorApi,
- CUSTOM_EVENTS.openInsertVideoModal,
- wrapper.vm.onOpenInsertVideoModal,
- );
- });
-
- it('registers HTML to markdown renderer', () => {
- expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi);
- });
-
- it('emits load event with the markdown formatted by Toast UI', () => {
- mockEditorApi.getMarkdown.mockReturnValueOnce(formattedMarkdown);
- expect(mockEditorApi.getMarkdown).toHaveBeenCalled();
- expect(wrapper.emitted('load')[0]).toEqual([{ formattedMarkdown }]);
- });
- });
-
- describe('when editor is destroyed', () => {
- beforeEach(() => {
- buildWrapper();
- });
-
- it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
- wrapper.vm.$destroy();
-
- expect(removeCustomEventListener).toHaveBeenCalledWith(
- wrapper.vm.editorApi,
- CUSTOM_EVENTS.openAddImageModal,
- wrapper.vm.onOpenAddImageModal,
- );
- });
-
- it('removes the CUSTOM_EVENTS.openInsertVideoModal custom event listener', () => {
- wrapper.vm.$destroy();
-
- expect(removeCustomEventListener).toHaveBeenCalledWith(
- wrapper.vm.editorApi,
- CUSTOM_EVENTS.openInsertVideoModal,
- wrapper.vm.onOpenInsertVideoModal,
- );
- });
- });
-
- describe('add image modal', () => {
- beforeEach(() => {
- buildWrapper();
- });
-
- it('renders an addImageModal component', () => {
- expect(findAddImageModal().exists()).toBe(true);
- });
-
- it('calls the onAddImage method when the addImage event is emitted', () => {
- const mockImage = { imageUrl: 'some/url.png', altText: 'some description' };
- const mockInstance = { exec: jest.fn() };
- wrapper.vm.$refs.editor = mockInstance;
-
- findAddImageModal().vm.$emit('addImage', mockImage);
- expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage, undefined);
- });
- });
-
- describe('insert video modal', () => {
- beforeEach(() => {
- buildWrapper();
- });
-
- it('renders an insertVideoModal component', () => {
- expect(findInsertVideoModal().exists()).toBe(true);
- });
-
- it('calls the onInsertVideo method when the insertVideo event is emitted', () => {
- const mockUrl = 'https://www.youtube.com/embed/someId';
- const mockInstance = { exec: jest.fn() };
- wrapper.vm.$refs.editor = mockInstance;
-
- findInsertVideoModal().vm.$emit('insertVideo', mockUrl);
- expect(insertVideo).toHaveBeenCalledWith(mockInstance, mockUrl);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/build_custom_renderer_spec.js
deleted file mode 100644
index 202e13e8bff..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/build_custom_renderer_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import buildCustomHTMLRenderer from '~/static_site_editor/rich_content_editor/services/build_custom_renderer';
-
-describe('Build Custom Renderer Service', () => {
- describe('buildCustomHTMLRenderer', () => {
- it('should return an object with the default renderer functions when lacking arguments', () => {
- expect(buildCustomHTMLRenderer()).toEqual(
- expect.objectContaining({
- htmlBlock: expect.any(Function),
- htmlInline: expect.any(Function),
- heading: expect.any(Function),
- item: expect.any(Function),
- paragraph: expect.any(Function),
- text: expect.any(Function),
- softbreak: expect.any(Function),
- }),
- );
- });
-
- it('should return an object with both custom and default renderer functions when passed customRenderers', () => {
- const mockHtmlCustomRenderer = jest.fn();
- const customRenderers = {
- html: [mockHtmlCustomRenderer],
- };
-
- expect(buildCustomHTMLRenderer(customRenderers)).toEqual(
- expect.objectContaining({
- html: expect.any(Function),
- }),
- );
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
deleted file mode 100644
index c9cba3e8689..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
+++ /dev/null
@@ -1,218 +0,0 @@
-import buildHTMLToMarkdownRenderer from '~/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer';
-import { attributeDefinition } from './renderers/mock_data';
-
-describe('rich_content_editor/services/html_to_markdown_renderer', () => {
- let baseRenderer;
- let htmlToMarkdownRenderer;
- let fakeNode;
-
- beforeEach(() => {
- baseRenderer = {
- trim: jest.fn((input) => `trimmed ${input}`),
- getSpaceCollapsedText: jest.fn((input) => `space collapsed ${input}`),
- getSpaceControlled: jest.fn((input) => `space controlled ${input}`),
- convert: jest.fn(),
- };
-
- fakeNode = { nodeValue: 'mock_node', dataset: {} };
- });
-
- afterEach(() => {
- htmlToMarkdownRenderer = null;
- });
-
- describe('TEXT_NODE visitor', () => {
- it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => {
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
-
- expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe(
- `space controlled trimmed space collapsed ${fakeNode.nodeValue}`,
- );
- });
- });
-
- describe('LI OL, LI UL visitor', () => {
- const oneLevelNestedList = '\n * List item 1\n * List item 2';
- const twoLevelNestedList = '\n * List item 1\n * List item 2';
- const spaceInContentList = '\n * List item 1\n * List item 2';
-
- it.each`
- list | indentSpaces | result
- ${oneLevelNestedList} | ${2} | ${'\n * List item 1\n * List item 2'}
- ${oneLevelNestedList} | ${3} | ${'\n * List item 1\n * List item 2'}
- ${oneLevelNestedList} | ${6} | ${'\n * List item 1\n * List item 2'}
- ${twoLevelNestedList} | ${4} | ${'\n * List item 1\n * List item 2'}
- ${spaceInContentList} | ${1} | ${'\n * List item 1\n * List item 2'}
- `('changes the list indentation to $indentSpaces spaces', ({ list, indentSpaces, result }) => {
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
- subListIndentSpaces: indentSpaces,
- });
-
- baseRenderer.convert.mockReturnValueOnce(list);
-
- expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list);
- });
- });
-
- describe('UL LI visitor', () => {
- it.each`
- listItem | unorderedListBulletChar | result | bulletChar
- ${'* list item'} | ${undefined} | ${'- list item'} | ${'default'}
- ${' - list item'} | ${'*'} | ${' * list item'} | ${'*'}
- ${' * list item'} | ${'-'} | ${' - list item'} | ${'-'}
- `(
- 'uses $bulletChar bullet char in unordered list items when $unorderedListBulletChar is set in config',
- ({ listItem, unorderedListBulletChar, result }) => {
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
- unorderedListBulletChar,
- });
- baseRenderer.convert.mockReturnValueOnce(listItem);
-
- expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, listItem);
- },
- );
-
- it('detects attribute definitions and attaches them to the list item', () => {
- const listItem = '- list item';
- const result = `${listItem}\n${attributeDefinition}\n`;
-
- fakeNode.dataset.attributeDefinition = attributeDefinition;
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
- baseRenderer.convert.mockReturnValueOnce(`${listItem}\n`);
-
- expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
- });
- });
-
- describe('OL LI visitor', () => {
- it.each`
- listItem | result | incrementListMarker | action
- ${'2. list item'} | ${'1. list item'} | ${false} | ${'increments'}
- ${' 3. list item'} | ${' 1. list item'} | ${false} | ${'increments'}
- ${' 123. list item'} | ${' 1. list item'} | ${false} | ${'increments'}
- ${'3. list item'} | ${'3. list item'} | ${true} | ${'does not increment'}
- `(
- '$action a list item counter when incrementListMaker is $incrementListMarker',
- ({ listItem, result, incrementListMarker }) => {
- const subContent = null;
-
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
- incrementListMarker,
- });
- baseRenderer.convert.mockReturnValueOnce(listItem);
-
- expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent);
- },
- );
- });
-
- describe('STRONG, B visitor', () => {
- it.each`
- input | strongCharacter | result
- ${'**strong text**'} | ${'_'} | ${'__strong text__'}
- ${'__strong text__'} | ${'*'} | ${'**strong text**'}
- `(
- 'converts $input to $result when strong character is $strongCharacter',
- ({ input, strongCharacter, result }) => {
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
- strong: strongCharacter,
- });
-
- baseRenderer.convert.mockReturnValueOnce(input);
-
- expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
- },
- );
- });
-
- describe('EM, I visitor', () => {
- it.each`
- input | emphasisCharacter | result
- ${'*strong text*'} | ${'_'} | ${'_strong text_'}
- ${'_strong text_'} | ${'*'} | ${'*strong text*'}
- `(
- 'converts $input to $result when emphasis character is $emphasisCharacter',
- ({ input, emphasisCharacter, result }) => {
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
- emphasis: emphasisCharacter,
- });
-
- baseRenderer.convert.mockReturnValueOnce(input);
-
- expect(htmlToMarkdownRenderer['EM, I'](fakeNode, input)).toBe(result);
- expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
- },
- );
- });
-
- describe('H1, H2, H3, H4, H5, H6 visitor', () => {
- it('detects attribute definitions and attaches them to the heading', () => {
- const heading = 'heading text';
- const result = `${heading.trimRight()}\n${attributeDefinition}\n\n`;
-
- fakeNode.dataset.attributeDefinition = attributeDefinition;
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
- baseRenderer.convert.mockReturnValueOnce(`${heading}\n\n`);
-
- expect(htmlToMarkdownRenderer['H1, H2, H3, H4, H5, H6'](fakeNode, heading)).toBe(result);
- });
- });
-
- describe('PRE CODE', () => {
- let node;
- const subContent = 'sub content';
- const originalConverterResult = 'base result';
-
- beforeEach(() => {
- node = document.createElement('PRE');
-
- node.innerText = 'reference definition content';
- node.dataset.sseReferenceDefinition = true;
-
- baseRenderer.convert.mockReturnValueOnce(originalConverterResult);
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
- });
-
- it('returns raw text when pre node has sse-reference-definitions class', () => {
- expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(
- `\n\n${node.innerText}\n\n`,
- );
- });
-
- it('returns base result when pre node does not have sse-reference-definitions class', () => {
- delete node.dataset.sseReferenceDefinition;
-
- expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(originalConverterResult);
- });
- });
-
- describe('IMG', () => {
- const originalSrc = 'path/to/image.png';
- const alt = 'alt text';
- let node;
-
- beforeEach(() => {
- node = document.createElement('img');
- node.alt = alt;
- node.src = originalSrc;
- });
-
- it('returns an image with its original src of the `original-src` attribute is preset', () => {
- node.dataset.originalSrc = originalSrc;
- node.src = 'modified/path/to/image.png';
-
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
-
- expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`);
- });
-
- it('fallback to `src` if no `original-src` is specified on the image', () => {
- htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
- expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token_spec.js
deleted file mode 100644
index ef3ff052cb2..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token_spec.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import {
- buildTextToken,
- buildUneditableOpenTokens,
- buildUneditableCloseToken,
- buildUneditableCloseTokens,
- buildUneditableBlockTokens,
- buildUneditableInlineTokens,
- buildUneditableHtmlAsTextTokens,
-} from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
-
-import {
- originInlineToken,
- originToken,
- uneditableOpenTokens,
- uneditableCloseToken,
- uneditableCloseTokens,
- uneditableBlockTokens,
- uneditableInlineTokens,
- uneditableTokens,
-} from './mock_data';
-
-describe('Build Uneditable Token renderer helper', () => {
- describe('buildTextToken', () => {
- it('returns an object literal representing a text token', () => {
- const text = originToken.content;
- expect(buildTextToken(text)).toStrictEqual(originToken);
- });
- });
-
- describe('buildUneditableOpenTokens', () => {
- it('returns a 2-item array of tokens with the originToken appended to an open token', () => {
- const result = buildUneditableOpenTokens(originToken);
-
- expect(result).toHaveLength(2);
- expect(result).toStrictEqual(uneditableOpenTokens);
- });
- });
-
- describe('buildUneditableCloseToken', () => {
- it('returns an object literal representing the uneditable close token', () => {
- expect(buildUneditableCloseToken()).toStrictEqual(uneditableCloseToken);
- });
- });
-
- describe('buildUneditableCloseTokens', () => {
- it('returns a 2-item array of tokens with the originToken prepended to a close token', () => {
- const result = buildUneditableCloseTokens(originToken);
-
- expect(result).toHaveLength(2);
- expect(result).toStrictEqual(uneditableCloseTokens);
- });
- });
-
- describe('buildUneditableBlockTokens', () => {
- it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => {
- const result = buildUneditableBlockTokens(originToken);
-
- expect(result).toHaveLength(3);
- expect(result).toStrictEqual(uneditableTokens);
- });
- });
-
- describe('buildUneditableInlineTokens', () => {
- it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => {
- const result = buildUneditableInlineTokens(originInlineToken);
-
- expect(result).toHaveLength(3);
- expect(result).toStrictEqual(uneditableInlineTokens);
- });
- });
-
- describe('buildUneditableHtmlAsTextTokens', () => {
- it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => {
- const htmlBlockNode = {
- type: 'htmlBlock',
- literal: '<div data-tomark-pass ><h1>Some header</h1><p>Some paragraph</p></div>',
- };
- const result = buildUneditableHtmlAsTextTokens(htmlBlockNode);
- const { type, content } = result[1];
-
- expect(type).toBe('text');
- expect(content).not.toMatch(/ data-tomark-pass /);
-
- expect(result).toHaveLength(3);
- expect(result).toStrictEqual(uneditableBlockTokens);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/mock_data.js
deleted file mode 100644
index 407072fb596..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/mock_data.js
+++ /dev/null
@@ -1,54 +0,0 @@
-// Node spec helpers
-
-export const buildMockTextNode = (literal) => ({ literal, type: 'text' });
-
-export const normalTextNode = buildMockTextNode('This is just normal text.');
-
-// Token spec helpers
-
-const buildMockUneditableOpenToken = (type) => {
- return {
- type: 'openTag',
- tagName: type,
- attributes: { contenteditable: false },
- classNames: [
- 'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
- ],
- };
-};
-
-const buildMockTextToken = (content) => {
- return {
- type: 'text',
- tagName: null,
- content,
- };
-};
-
-const buildMockUneditableCloseToken = (type) => ({ type: 'closeTag', tagName: type });
-
-export const originToken = buildMockTextToken('{:.no_toc .hidden-md .hidden-lg}');
-const uneditableOpenToken = buildMockUneditableOpenToken('div');
-export const uneditableOpenTokens = [uneditableOpenToken, originToken];
-export const uneditableCloseToken = buildMockUneditableCloseToken('div');
-export const uneditableCloseTokens = [originToken, uneditableCloseToken];
-export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
-
-export const originInlineToken = {
- type: 'text',
- content: '<i>Inline</i> content',
-};
-
-export const uneditableInlineTokens = [
- buildMockUneditableOpenToken('a'),
- originInlineToken,
- buildMockUneditableCloseToken('a'),
-];
-
-export const uneditableBlockTokens = [
- uneditableOpenToken,
- buildMockTextToken('<div><h1>Some header</h1><p>Some paragraph</p></div>'),
- uneditableCloseToken,
-];
-
-export const attributeDefinition = '{:.no_toc .hidden-md .hidden-lg}';
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition_spec.js
deleted file mode 100644
index 6d96dd3bbca..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition_spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition';
-import { attributeDefinition } from './mock_data';
-
-describe('rich_content_editor/renderers/render_attribute_definition', () => {
- describe('canRender', () => {
- it.each`
- input | result
- ${{ literal: attributeDefinition }} | ${true}
- ${{ literal: `FOO${attributeDefinition}` }} | ${false}
- ${{ literal: `${attributeDefinition}BAR` }} | ${false}
- ${{ literal: 'foobar' }} | ${false}
- `('returns $result when input is $input', ({ input, result }) => {
- expect(renderer.canRender(input)).toBe(result);
- });
- });
-
- describe('render', () => {
- it('returns an empty HTML comment', () => {
- expect(renderer.render()).toEqual({
- type: 'html',
- content: '<!-- sse-attribute-definition -->',
- });
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
deleted file mode 100644
index 29e2b5b3b16..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text';
-import { renderUneditableLeaf } from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
-
-import { buildMockTextNode, normalTextNode } from './mock_data';
-
-const embeddedRubyTextNode = buildMockTextNode('<%= partial("some/path") %>');
-
-describe('Render Embedded Ruby Text renderer', () => {
- describe('canRender', () => {
- it('should return true when the argument `literal` has embedded ruby syntax', () => {
- expect(renderer.canRender(embeddedRubyTextNode)).toBe(true);
- });
-
- it('should return false when the argument `literal` lacks embedded ruby syntax', () => {
- expect(renderer.canRender(normalTextNode)).toBe(false);
- });
- });
-
- describe('render', () => {
- it('should delegate rendering to the renderUneditableLeaf util', () => {
- expect(renderer.render).toBe(renderUneditableLeaf);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
deleted file mode 100644
index 0fda847b688..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { buildUneditableInlineTokens } from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline';
-
-import { normalTextNode } from './mock_data';
-
-const fontAwesomeInlineHtmlNode = {
- firstChild: null,
- literal: '<i class="far fa-paper-plane" id="biz-tech-icons">',
- type: 'html',
-};
-
-describe('Render Font Awesome Inline HTML renderer', () => {
- describe('canRender', () => {
- it('should return true when the argument `literal` has font awesome inline html syntax', () => {
- expect(renderer.canRender(fontAwesomeInlineHtmlNode)).toBe(true);
- });
-
- it('should return false when the argument `literal` lacks font awesome inline html syntax', () => {
- expect(renderer.canRender(normalTextNode)).toBe(false);
- });
- });
-
- describe('render', () => {
- it('should return uneditable inline tokens', () => {
- const token = { type: 'text', tagName: null, content: fontAwesomeInlineHtmlNode.literal };
- const context = { origin: () => token };
-
- expect(renderer.render(fontAwesomeInlineHtmlNode, context)).toStrictEqual(
- buildUneditableInlineTokens(token),
- );
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_heading_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_heading_spec.js
deleted file mode 100644
index cf4a90885df..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_heading_spec.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_heading';
-import * as renderUtils from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
-
-describe('rich_content_editor/renderers/render_heading', () => {
- it('canRender delegates to renderUtils.willAlwaysRender', () => {
- expect(renderer.canRender).toBe(renderUtils.willAlwaysRender);
- });
-
- it('render delegates to renderUtils.renderWithAttributeDefinitions', () => {
- expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions);
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_html_block_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_html_block_spec.js
deleted file mode 100644
index 9c937ac22f4..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_html_block_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { buildUneditableHtmlAsTextTokens } from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_html_block';
-
-describe('rich_content_editor/services/renderers/render_html_block', () => {
- const htmlBlockNode = {
- literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
- type: 'htmlBlock',
- };
-
- describe('canRender', () => {
- it.each`
- input | result
- ${htmlBlockNode} | ${true}
- ${{ literal: '<iframe></iframe>', type: 'htmlBlock' }} | ${true}
- ${{ literal: '<iframe src="https://www.youtube.com"></iframe>', type: 'htmlBlock' }} | ${false}
- ${{ literal: '<iframe></iframe>', type: 'text' }} | ${false}
- `('returns $result when input=$input', ({ input, result }) => {
- expect(renderer.canRender(input)).toBe(result);
- });
- });
-
- describe('render', () => {
- const htmlBlockNodeToMark = {
- firstChild: null,
- literal: '<div data-to-mark ></div>',
- type: 'htmlBlock',
- };
-
- it.each`
- node
- ${htmlBlockNode}
- ${htmlBlockNodeToMark}
- `('should return uneditable tokens wrapping the $node as a token', ({ node }) => {
- expect(renderer.render(node)).toStrictEqual(buildUneditableHtmlAsTextTokens(node));
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
deleted file mode 100644
index 15fb2c3a430..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { buildUneditableInlineTokens } from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text';
-
-import { buildMockTextNode, normalTextNode } from './mock_data';
-
-const mockTextStart = 'Majority example ';
-const mockTextMiddle = '[environment terraform plans][terraform]';
-const mockTextEnd = '.';
-const identifierInstanceStartTextNode = buildMockTextNode(mockTextStart);
-const identifierInstanceEndTextNode = buildMockTextNode(mockTextEnd);
-
-describe('Render Identifier Instance Text renderer', () => {
- describe('canRender', () => {
- it.each`
- node | target
- ${normalTextNode} | ${false}
- ${identifierInstanceStartTextNode} | ${false}
- ${identifierInstanceEndTextNode} | ${false}
- ${buildMockTextNode(mockTextMiddle)} | ${true}
- ${buildMockTextNode('Minority example [environment terraform plans][]')} | ${true}
- ${buildMockTextNode('Minority example [environment terraform plans]')} | ${true}
- `(
- 'should return $target when the $node validates against identifier instance syntax',
- ({ node, target }) => {
- expect(renderer.canRender(node)).toBe(target);
- },
- );
- });
-
- describe('render', () => {
- it.each`
- start | middle | end
- ${mockTextStart} | ${mockTextMiddle} | ${mockTextEnd}
- ${mockTextStart} | ${'[environment terraform plans][]'} | ${mockTextEnd}
- ${mockTextStart} | ${'[environment terraform plans]'} | ${mockTextEnd}
- `(
- 'should return inline editable, uneditable, and editable tokens in sequence',
- ({ start, middle, end }) => {
- const buildMockTextToken = (content) => ({ type: 'text', tagName: null, content });
-
- const startToken = buildMockTextToken(start);
- const middleToken = buildMockTextToken(middle);
- const endToken = buildMockTextToken(end);
-
- const content = `${start}${middle}${end}`;
- const contentToken = buildMockTextToken(content);
- const contentNode = buildMockTextNode(content);
- const context = { origin: jest.fn().mockReturnValueOnce(contentToken) };
- expect(renderer.render(contentNode, context)).toStrictEqual(
- [startToken, buildUneditableInlineTokens(middleToken), endToken].flat(),
- );
- },
- );
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
deleted file mode 100644
index ddc96ed6832..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph';
-
-import { buildMockTextNode } from './mock_data';
-
-const buildMockParagraphNode = (literal) => {
- return {
- firstChild: buildMockTextNode(literal),
- type: 'paragraph',
- };
-};
-
-const normalParagraphNode = buildMockParagraphNode(
- 'This is just normal paragraph. It has multiple sentences.',
-);
-const identifierParagraphNode = buildMockParagraphNode(
- `[another-identifier]: https://example.com "This example has a title" [identifier]: http://example1.com [this link]: http://example.org`,
-);
-
-describe('rich_content_editor/renderers_render_identifier_paragraph', () => {
- describe('canRender', () => {
- it.each`
- node | paragraph | target
- ${identifierParagraphNode} | ${'[Some text]: https://link.com'} | ${true}
- ${normalParagraphNode} | ${'Normal non-identifier text. Another sentence.'} | ${false}
- `(
- 'should return $target when the $node matches $paragraph syntax',
- ({ node, paragraph, target }) => {
- const context = {
- entering: true,
- getChildrenText: jest.fn().mockReturnValueOnce(paragraph),
- };
-
- expect(renderer.canRender(node, context)).toBe(target);
- },
- );
- });
-
- describe('render', () => {
- let context;
- let result;
-
- beforeEach(() => {
- const node = {
- firstChild: {
- type: 'text',
- literal: '[Some text]: https://link.com',
- next: {
- type: 'linebreak',
- next: {
- type: 'text',
- literal: '[identifier]: http://example1.com "title"',
- },
- },
- },
- };
- context = { skipChildren: jest.fn() };
- result = renderer.render(node, context);
- });
-
- it('renders the reference definitions as a code block', () => {
- expect(result).toEqual([
- {
- type: 'openTag',
- tagName: 'pre',
- classNames: ['code-block', 'language-markdown'],
- attributes: {
- 'data-sse-reference-definition': true,
- },
- },
- { type: 'openTag', tagName: 'code' },
- {
- type: 'text',
- content: '[Some text]: https://link.com\n[identifier]: http://example1.com "title"',
- },
- { type: 'closeTag', tagName: 'code' },
- { type: 'closeTag', tagName: 'pre' },
- ]);
- });
-
- it('skips the reference definition node children from rendering', () => {
- expect(context.skipChildren).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_list_item_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_list_item_spec.js
deleted file mode 100644
index 1e8e62b9dd2..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_list_item_spec.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_list_item';
-import * as renderUtils from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
-
-describe('rich_content_editor/renderers/render_list_item', () => {
- it('canRender delegates to renderUtils.willAlwaysRender', () => {
- expect(renderer.canRender).toBe(renderUtils.willAlwaysRender);
- });
-
- it('render delegates to renderUtils.renderWithAttributeDefinitions', () => {
- expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions);
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_softbreak_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_softbreak_spec.js
deleted file mode 100644
index d8d1e6ff295..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_softbreak_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_softbreak';
-
-describe('Render softbreak renderer', () => {
- describe('canRender', () => {
- it.each`
- node | parentType | result
- ${{ parent: { type: 'emph' } }} | ${'emph'} | ${true}
- ${{ parent: { type: 'strong' } }} | ${'strong'} | ${true}
- ${{ parent: { type: 'paragraph' } }} | ${'paragraph'} | ${false}
- `('returns $result when node parent type is $parentType ', ({ node, result }) => {
- expect(renderer.canRender(node)).toBe(result);
- });
- });
-
- describe('render', () => {
- it('returns text node with a break line', () => {
- expect(renderer.render()).toEqual({
- type: 'text',
- content: ' ',
- });
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_utils_spec.js
deleted file mode 100644
index 49b8936a9f7..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_utils_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import {
- buildUneditableBlockTokens,
- buildUneditableOpenTokens,
-} from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
-import {
- renderUneditableLeaf,
- renderUneditableBranch,
- renderWithAttributeDefinitions,
- willAlwaysRender,
-} from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
-
-import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data';
-
-describe('rich_content_editor/renderers/render_utils', () => {
- describe('renderUneditableLeaf', () => {
- it('should return uneditable block tokens around an origin token', () => {
- const context = { origin: jest.fn().mockReturnValueOnce(originToken) };
- const result = renderUneditableLeaf({}, context);
-
- expect(result).toStrictEqual(buildUneditableBlockTokens(originToken));
- });
- });
-
- describe('renderUneditableBranch', () => {
- let origin;
-
- beforeEach(() => {
- origin = jest.fn().mockReturnValueOnce(originToken);
- });
-
- it('should return uneditable block open token followed by the origin token when entering', () => {
- const context = { entering: true, origin };
- const result = renderUneditableBranch({}, context);
-
- expect(result).toStrictEqual(buildUneditableOpenTokens(originToken));
- });
-
- it('should return uneditable block closing token when exiting', () => {
- const context = { entering: false, origin };
- const result = renderUneditableBranch({}, context);
-
- expect(result).toStrictEqual(uneditableCloseToken);
- });
- });
-
- describe('willAlwaysRender', () => {
- it('always returns true', () => {
- expect(willAlwaysRender()).toBe(true);
- });
- });
-
- describe('renderWithAttributeDefinitions', () => {
- let openTagToken;
- let closeTagToken;
- let node;
- const attributes = {
- 'data-attribute-definition': attributeDefinition,
- };
-
- beforeEach(() => {
- openTagToken = { type: 'openTag' };
- closeTagToken = { type: 'closeTag' };
- node = {
- next: {
- firstChild: {
- literal: attributeDefinition,
- },
- },
- };
- });
-
- describe('when token type is openTag', () => {
- it('attaches attributes when attributes exist in the node’s next sibling', () => {
- const context = { origin: () => openTagToken };
-
- expect(renderWithAttributeDefinitions(node, context)).toEqual({
- ...openTagToken,
- attributes,
- });
- });
-
- it('attaches attributes when attributes exist in the node’s children', () => {
- const context = { origin: () => openTagToken };
- node = {
- firstChild: {
- firstChild: {
- next: {
- next: {
- literal: attributeDefinition,
- },
- },
- },
- },
- };
-
- expect(renderWithAttributeDefinitions(node, context)).toEqual({
- ...openTagToken,
- attributes,
- });
- });
- });
-
- it('does not attach attributes when token type is "closeTag"', () => {
- const context = { origin: () => closeTagToken };
-
- expect(renderWithAttributeDefinitions({}, context)).toBe(closeTagToken);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/services/sanitize_html_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/sanitize_html_spec.js
deleted file mode 100644
index 2f2d3beb53d..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/services/sanitize_html_spec.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import sanitizeHTML from '~/static_site_editor/rich_content_editor/services/sanitize_html';
-
-describe('rich_content_editor/services/sanitize_html', () => {
- it.each`
- input | result
- ${'<iframe src="https://www.youtube.com"></iframe>'} | ${'<iframe src="https://www.youtube.com"></iframe>'}
- ${'<iframe src="https://gitlab.com"></iframe>'} | ${''}
- `('removes iframes if the iframe source origin is not allowed', ({ input, result }) => {
- expect(sanitizeHTML(input)).toBe(result);
- });
-});
diff --git a/spec/frontend/static_site_editor/rich_content_editor/toolbar_item_spec.js b/spec/frontend/static_site_editor/rich_content_editor/toolbar_item_spec.js
deleted file mode 100644
index c9dcf9cfe2e..00000000000
--- a/spec/frontend/static_site_editor/rich_content_editor/toolbar_item_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ToolbarItem from '~/static_site_editor/rich_content_editor/toolbar_item.vue';
-
-describe('Toolbar Item', () => {
- let wrapper;
-
- const findIcon = () => wrapper.find(GlIcon);
- const findButton = () => wrapper.find('button');
-
- const buildWrapper = (propsData) => {
- wrapper = shallowMount(ToolbarItem, {
- propsData,
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- describe.each`
- icon | tooltip
- ${'heading'} | ${'Headings'}
- ${'bold'} | ${'Add bold text'}
- ${'italic'} | ${'Add italic text'}
- ${'strikethrough'} | ${'Add strikethrough text'}
- ${'quote'} | ${'Insert a quote'}
- ${'link'} | ${'Add a link'}
- ${'doc-code'} | ${'Insert a code block'}
- ${'list-bulleted'} | ${'Add a bullet list'}
- ${'list-numbered'} | ${'Add a numbered list'}
- ${'list-task'} | ${'Add a task list'}
- ${'list-indent'} | ${'Indent'}
- ${'list-outdent'} | ${'Outdent'}
- ${'dash'} | ${'Add a line'}
- ${'table'} | ${'Add a table'}
- ${'code'} | ${'Insert an image'}
- ${'code'} | ${'Insert inline code'}
- `('toolbar item component', ({ icon, tooltip }) => {
- beforeEach(() => buildWrapper({ icon, tooltip }));
-
- it('renders a toolbar button', () => {
- expect(findButton().exists()).toBe(true);
- });
-
- it('renders the correct tooltip', () => {
- const buttonTooltip = getBinding(wrapper.element, 'gl-tooltip');
- expect(buttonTooltip).toBeDefined();
- expect(buttonTooltip.value.title).toBe(tooltip);
- });
-
- it(`renders the ${icon} icon`, () => {
- expect(findIcon().exists()).toBe(true);
- expect(findIcon().props().name).toBe(icon);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/services/formatter_spec.js b/spec/frontend/static_site_editor/services/formatter_spec.js
deleted file mode 100644
index 9e9c4bbd171..00000000000
--- a/spec/frontend/static_site_editor/services/formatter_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import formatter from '~/static_site_editor/services/formatter';
-
-describe('static_site_editor/services/formatter', () => {
- const source = `Some text
-<br>
-
-And some more text
-
-
-<br>
-
-
-And even more text`;
- const sourceWithoutBrTags = `Some text
-
-And some more text
-
-
-
-
-And even more text`;
-
- it('removes extraneous <br> tags', () => {
- expect(formatter(source)).toMatch(sourceWithoutBrTags);
- });
-
- describe('ordered lists with incorrect content indentation', () => {
- it.each`
- input | result
- ${'12. ordered list item\n13.Next ordered list item'} | ${'12. ordered list item\n13.Next ordered list item'}
- ${'12. ordered list item\n - Next ordered list item'} | ${'12. ordered list item\n - Next ordered list item'}
- ${'12. ordered list item\n - Next ordered list item'} | ${'12. ordered list item\n - Next ordered list item'}
- ${'12. ordered list item\n Next ordered list item'} | ${'12. ordered list item\n Next ordered list item'}
- ${'1. ordered list item\n Next ordered list item'} | ${'1. ordered list item\n Next ordered list item'}
- `('\ntransforms\n$input \nto\n$result', ({ input, result }) => {
- expect(formatter(input)).toBe(result);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/services/front_matterify_spec.js b/spec/frontend/static_site_editor/services/front_matterify_spec.js
deleted file mode 100644
index ec3752b30c6..00000000000
--- a/spec/frontend/static_site_editor/services/front_matterify_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { frontMatterify, stringify } from '~/static_site_editor/services/front_matterify';
-import {
- sourceContentYAML as content,
- sourceContentHeaderObjYAML as yamlFrontMatterObj,
- sourceContentSpacing as spacing,
- sourceContentBody as body,
-} from '../mock_data';
-
-describe('static_site_editor/services/front_matterify', () => {
- const frontMatterifiedContent = {
- source: content,
- matter: yamlFrontMatterObj,
- hasMatter: true,
- spacing,
- content: body,
- delimiter: '---',
- type: 'yaml',
- };
- const frontMatterifiedBody = {
- source: body,
- matter: null,
- hasMatter: false,
- spacing: null,
- content: body,
- delimiter: null,
- type: null,
- };
-
- describe('frontMatterify', () => {
- it.each`
- frontMatterified | target
- ${frontMatterify(content)} | ${frontMatterifiedContent}
- ${frontMatterify(body)} | ${frontMatterifiedBody}
- `('returns $target from $frontMatterified', ({ frontMatterified, target }) => {
- expect(frontMatterified).toEqual(target);
- });
-
- it('should throw when matter is invalid', () => {
- const invalidContent = `---\nkey: val\nkeyNoVal\n---\n${body}`;
-
- expect(() => frontMatterify(invalidContent)).toThrow();
- });
- });
-
- describe('stringify', () => {
- it.each`
- stringified | target
- ${stringify(frontMatterifiedContent)} | ${content}
- ${stringify(frontMatterifiedBody)} | ${body}
- `('returns $target from $stringified', ({ stringified, target }) => {
- expect(stringified).toBe(target);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/services/generate_branch_name_spec.js b/spec/frontend/static_site_editor/services/generate_branch_name_spec.js
deleted file mode 100644
index 7e437506a16..00000000000
--- a/spec/frontend/static_site_editor/services/generate_branch_name_spec.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { BRANCH_SUFFIX_COUNT } from '~/static_site_editor/constants';
-import generateBranchName from '~/static_site_editor/services/generate_branch_name';
-
-import { username, branch as targetBranch } from '../mock_data';
-
-describe('generateBranchName', () => {
- const timestamp = 12345678901234;
-
- beforeEach(() => {
- jest.spyOn(Date, 'now').mockReturnValueOnce(timestamp);
- });
-
- it('generates a name that includes the username and target branch', () => {
- expect(generateBranchName(username, targetBranch)).toMatch(`${username}-${targetBranch}`);
- });
-
- it(`adds the first ${BRANCH_SUFFIX_COUNT} numbers of the current timestamp`, () => {
- expect(generateBranchName(username, targetBranch)).toMatch(
- timestamp.toString().substring(BRANCH_SUFFIX_COUNT),
- );
- });
-});
diff --git a/spec/frontend/static_site_editor/services/load_source_content_spec.js b/spec/frontend/static_site_editor/services/load_source_content_spec.js
deleted file mode 100644
index 98d437698c4..00000000000
--- a/spec/frontend/static_site_editor/services/load_source_content_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Api from '~/api';
-
-import loadSourceContent from '~/static_site_editor/services/load_source_content';
-
-import {
- sourceContentYAML as sourceContent,
- sourceContentTitle,
- projectId,
- sourcePath,
-} from '../mock_data';
-
-describe('loadSourceContent', () => {
- describe('requesting source content succeeds', () => {
- let result;
-
- beforeEach(() => {
- jest.spyOn(Api, 'getRawFile').mockResolvedValue({ data: sourceContent });
-
- return loadSourceContent({ projectId, sourcePath }).then((_result) => {
- result = _result;
- });
- });
-
- it('calls getRawFile API with project id and source path', () => {
- expect(Api.getRawFile).toHaveBeenCalledWith(projectId, sourcePath);
- });
-
- it('extracts page title from source content', () => {
- expect(result.title).toBe(sourceContentTitle);
- });
-
- it('returns raw content', () => {
- expect(result.content).toBe(sourceContent);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/services/parse_source_file_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
deleted file mode 100644
index fdd11297e09..00000000000
--- a/spec/frontend/static_site_editor/services/parse_source_file_spec.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import parseSourceFile from '~/static_site_editor/services/parse_source_file';
-import {
- sourceContentYAML as content,
- sourceContentHeaderYAML as yamlFrontMatter,
- sourceContentHeaderObjYAML as yamlFrontMatterObj,
- sourceContentBody as body,
-} from '../mock_data';
-
-describe('static_site_editor/services/parse_source_file', () => {
- const contentComplex = [content, content, content].join('');
- const complexBody = [body, content, content].join('');
- const edit = 'and more';
- const newContent = `${content} ${edit}`;
- const newContentComplex = `${contentComplex} ${edit}`;
-
- describe('unmodified front matter', () => {
- it.each`
- parsedSource
- ${parseSourceFile(content)}
- ${parseSourceFile(contentComplex)}
- `('returns $targetFrontMatter when frontMatter queried', ({ parsedSource }) => {
- expect(parsedSource.matter()).toEqual(yamlFrontMatterObj);
- });
- });
-
- describe('unmodified content', () => {
- it.each`
- parsedSource
- ${parseSourceFile(content)}
- ${parseSourceFile(contentComplex)}
- `('returns false by default', ({ parsedSource }) => {
- expect(parsedSource.isModified()).toBe(false);
- });
-
- it.each`
- parsedSource | isBody | target
- ${parseSourceFile(content)} | ${undefined} | ${content}
- ${parseSourceFile(content)} | ${false} | ${content}
- ${parseSourceFile(content)} | ${true} | ${body}
- ${parseSourceFile(contentComplex)} | ${undefined} | ${contentComplex}
- ${parseSourceFile(contentComplex)} | ${false} | ${contentComplex}
- ${parseSourceFile(contentComplex)} | ${true} | ${complexBody}
- `(
- 'returns only the $target content when the `isBody` parameter argument is $isBody',
- ({ parsedSource, isBody, target }) => {
- expect(parsedSource.content(isBody)).toBe(target);
- },
- );
- });
-
- describe('modified front matter', () => {
- const newYamlFrontMatter = '---\nnewKey: newVal\n---';
- const newYamlFrontMatterObj = { newKey: 'newVal' };
- const contentWithNewFrontMatter = content.replace(yamlFrontMatter, newYamlFrontMatter);
- const contentComplexWithNewFrontMatter = contentComplex.replace(
- yamlFrontMatter,
- newYamlFrontMatter,
- );
-
- it.each`
- parsedSource | targetContent
- ${parseSourceFile(content)} | ${contentWithNewFrontMatter}
- ${parseSourceFile(contentComplex)} | ${contentComplexWithNewFrontMatter}
- `(
- 'returns the correct front matter and modified content',
- ({ parsedSource, targetContent }) => {
- expect(parsedSource.matter()).toMatchObject(yamlFrontMatterObj);
-
- parsedSource.syncMatter(newYamlFrontMatterObj);
-
- expect(parsedSource.matter()).toMatchObject(newYamlFrontMatterObj);
- expect(parsedSource.content()).toBe(targetContent);
- },
- );
- });
-
- describe('modified content', () => {
- const newBody = `${body} ${edit}`;
- const newComplexBody = `${complexBody} ${edit}`;
-
- it.each`
- parsedSource | hasMatter | isModified | targetRaw | targetBody
- ${parseSourceFile(content)} | ${true} | ${false} | ${content} | ${body}
- ${parseSourceFile(content)} | ${true} | ${true} | ${newContent} | ${newBody}
- ${parseSourceFile(contentComplex)} | ${true} | ${false} | ${contentComplex} | ${complexBody}
- ${parseSourceFile(contentComplex)} | ${true} | ${true} | ${newContentComplex} | ${newComplexBody}
- ${parseSourceFile(body)} | ${false} | ${false} | ${body} | ${body}
- ${parseSourceFile(body)} | ${false} | ${true} | ${newBody} | ${newBody}
- `(
- 'returns $isModified after a $targetRaw sync',
- ({ parsedSource, hasMatter, isModified, targetRaw, targetBody }) => {
- parsedSource.syncContent(targetRaw);
-
- expect(parsedSource.hasMatter()).toBe(hasMatter);
- expect(parsedSource.isModified()).toBe(isModified);
- expect(parsedSource.content()).toBe(targetRaw);
- expect(parsedSource.content(true)).toBe(targetBody);
- },
- );
- });
-});
diff --git a/spec/frontend/static_site_editor/services/renderers/render_image_spec.js b/spec/frontend/static_site_editor/services/renderers/render_image_spec.js
deleted file mode 100644
index d3298aa0b26..00000000000
--- a/spec/frontend/static_site_editor/services/renderers/render_image_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import imageRenderer from '~/static_site_editor/services/renderers/render_image';
-import { mounts, project, branch, baseUrl } from '../../mock_data';
-
-describe('rich_content_editor/renderers/render_image', () => {
- let renderer;
- let imageRepository;
-
- beforeEach(() => {
- renderer = imageRenderer.build(mounts, project, branch, baseUrl, imageRepository);
- imageRepository = { get: () => null };
- });
-
- describe('build', () => {
- it('builds a renderer object containing `canRender` and `render` functions', () => {
- expect(renderer).toHaveProperty('canRender', expect.any(Function));
- expect(renderer).toHaveProperty('render', expect.any(Function));
- });
- });
-
- describe('canRender', () => {
- it.each`
- input | result
- ${{ type: 'image' }} | ${true}
- ${{ type: 'text' }} | ${false}
- ${{ type: 'htmlBlock' }} | ${false}
- `('returns $result when input is $input', ({ input, result }) => {
- expect(renderer.canRender(input)).toBe(result);
- });
- });
-
- describe('render', () => {
- let skipChildren;
- let context;
- let node;
-
- beforeEach(() => {
- skipChildren = jest.fn();
- context = { skipChildren };
- node = {
- firstChild: {
- type: 'img',
- literal: 'Some Image',
- },
- };
- });
-
- it.each`
- destination | isAbsolute | src
- ${'http://test.host/absolute/path/to/image.png'} | ${true} | ${'http://test.host/absolute/path/to/image.png'}
- ${'/relative/path/to/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/default/source/relative/path/to/image.png'}
- ${'/target/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/source/with/target/image.png'}
- ${'relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/relative/to/current/image.png'}
- ${'./relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/./relative/to/current/image.png'}
- ${'../relative/to/current/image.png'} | ${false} | ${'http://test.host/user1/project1/-/raw/main/../relative/to/current/image.png'}
- `('returns an image with the correct attributes', ({ destination, isAbsolute, src }) => {
- node.destination = destination;
-
- const result = renderer.render(node, context);
-
- expect(result).toEqual({
- type: 'openTag',
- tagName: 'img',
- selfClose: true,
- attributes: {
- 'data-original-src': !isAbsolute ? destination : '',
- src,
- alt: 'Some Image',
- },
- });
-
- expect(skipChildren).toHaveBeenCalled();
- });
-
- it('renders an image if a cached image is found in the repository, use the base64 content as the source', () => {
- const imageContent = 'some-content';
- const originalSrc = 'path/to/image.png';
-
- imageRepository.get = () => imageContent;
- renderer = imageRenderer.build(mounts, project, branch, baseUrl, imageRepository);
- node.destination = originalSrc;
-
- const result = renderer.render(node, context);
-
- expect(result).toEqual({
- type: 'openTag',
- tagName: 'img',
- selfClose: true,
- attributes: {
- 'data-original-src': originalSrc,
- src: `data:image;base64,${imageContent}`,
- alt: 'Some Image',
- },
- });
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
deleted file mode 100644
index 757611166d7..00000000000
--- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
+++ /dev/null
@@ -1,261 +0,0 @@
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import Api from '~/api';
-import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
-
-import {
- SUBMIT_CHANGES_BRANCH_ERROR,
- SUBMIT_CHANGES_COMMIT_ERROR,
- SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
- TRACKING_ACTION_CREATE_COMMIT,
- TRACKING_ACTION_CREATE_MERGE_REQUEST,
- SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
- SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
- DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
- DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
-} from '~/static_site_editor/constants';
-import generateBranchName from '~/static_site_editor/services/generate_branch_name';
-import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
-
-import {
- username,
- projectId,
- commitBranchResponse,
- commitMultipleResponse,
- createMergeRequestResponse,
- mergeRequestMeta,
- sourcePath,
- branch as targetBranch,
- sourceContentYAML as content,
- trackingCategory,
- images,
-} from '../mock_data';
-
-jest.mock('~/static_site_editor/services/generate_branch_name');
-
-describe('submitContentChanges', () => {
- const sourceBranch = 'branch-name';
- let trackingSpy;
- let origPage;
-
- const buildPayload = (overrides = {}) => ({
- username,
- projectId,
- sourcePath,
- targetBranch,
- content,
- images,
- mergeRequestMeta,
- ...overrides,
- });
-
- beforeEach(() => {
- jest.spyOn(Api, 'createBranch').mockResolvedValue({ data: commitBranchResponse });
- jest.spyOn(Api, 'commitMultiple').mockResolvedValue({ data: commitMultipleResponse });
- jest
- .spyOn(Api, 'createProjectMergeRequest')
- .mockResolvedValue({ data: createMergeRequestResponse });
-
- generateBranchName.mockReturnValue(sourceBranch);
-
- origPage = document.body.dataset.page;
- document.body.dataset.page = trackingCategory;
- trackingSpy = mockTracking(document.body.dataset.page, undefined, jest.spyOn);
- });
-
- afterEach(() => {
- document.body.dataset.page = origPage;
- unmockTracking();
- });
-
- it('creates a branch named after the username and target branch', () => {
- return submitContentChanges(buildPayload()).then(() => {
- expect(Api.createBranch).toHaveBeenCalledWith(projectId, {
- ref: targetBranch,
- branch: sourceBranch,
- });
- });
- });
-
- it('notifies error when branch could not be created', () => {
- Api.createBranch.mockRejectedValueOnce();
-
- return expect(submitContentChanges(buildPayload())).rejects.toThrow(
- SUBMIT_CHANGES_BRANCH_ERROR,
- );
- });
-
- describe('committing markdown formatting changes', () => {
- const formattedMarkdown = `formatted ${content}`;
- const commitPayload = {
- branch: sourceBranch,
- commit_message: `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`,
- actions: [
- {
- action: 'update',
- file_path: sourcePath,
- content: formattedMarkdown,
- },
- ],
- };
-
- it('commits markdown formatting changes in a separate commit', () => {
- return submitContentChanges(buildPayload({ formattedMarkdown })).then(() => {
- expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, commitPayload);
- });
- });
-
- it('does not commit markdown formatting changes when there are none', () => {
- return submitContentChanges(buildPayload()).then(() => {
- expect(Api.commitMultiple.mock.calls).toHaveLength(1);
- expect(Api.commitMultiple.mock.calls[0][1]).not.toMatchObject({
- actions: commitPayload.actions,
- });
- });
- });
- });
-
- it('commits the content changes to the branch when creating branch succeeds', () => {
- return submitContentChanges(buildPayload()).then(() => {
- expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, {
- branch: sourceBranch,
- commit_message: mergeRequestMeta.title,
- actions: [
- {
- action: 'update',
- file_path: sourcePath,
- content,
- },
- {
- action: 'create',
- content: 'image1-content',
- encoding: 'base64',
- file_path: 'path/to/image1.png',
- },
- ],
- });
- });
- });
-
- it('does not commit an image if it has been removed from the content', () => {
- const contentWithoutImages = '## Content without images';
- const payload = buildPayload({ content: contentWithoutImages });
- return submitContentChanges(payload).then(() => {
- expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, {
- branch: sourceBranch,
- commit_message: mergeRequestMeta.title,
- actions: [
- {
- action: 'update',
- file_path: sourcePath,
- content: contentWithoutImages,
- },
- ],
- });
- });
- });
-
- it('notifies error when content could not be committed', () => {
- Api.commitMultiple.mockRejectedValueOnce();
-
- return expect(submitContentChanges(buildPayload())).rejects.toThrow(
- SUBMIT_CHANGES_COMMIT_ERROR,
- );
- });
-
- it('creates a merge request when committing changes succeeds', () => {
- return submitContentChanges(buildPayload()).then(() => {
- const { title, description } = mergeRequestMeta;
- expect(Api.createProjectMergeRequest).toHaveBeenCalledWith(
- projectId,
- convertObjectPropsToSnakeCase({
- title,
- description,
- targetBranch,
- sourceBranch,
- }),
- );
- });
- });
-
- it('notifies error when merge request could not be created', () => {
- Api.createProjectMergeRequest.mockRejectedValueOnce();
-
- return expect(submitContentChanges(buildPayload())).rejects.toThrow(
- SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
- );
- });
-
- describe('when changes are submitted successfully', () => {
- let result;
-
- beforeEach(() => {
- return submitContentChanges(buildPayload()).then((_result) => {
- result = _result;
- });
- });
-
- it('returns the branch name', () => {
- expect(result).toMatchObject({ branch: { label: sourceBranch } });
- });
-
- it('returns commit short id and web url', () => {
- expect(result).toMatchObject({
- commit: {
- label: commitMultipleResponse.short_id,
- url: commitMultipleResponse.web_url,
- },
- });
- });
-
- it('returns merge request iid and web url', () => {
- expect(result).toMatchObject({
- mergeRequest: {
- label: createMergeRequestResponse.iid,
- url: createMergeRequestResponse.web_url,
- },
- });
- });
- });
-
- describe('sends the correct tracking event', () => {
- beforeEach(() => {
- return submitContentChanges(buildPayload());
- });
-
- it('for committing changes', () => {
- expect(trackingSpy).toHaveBeenCalledWith(
- document.body.dataset.page,
- TRACKING_ACTION_CREATE_COMMIT,
- );
- });
-
- it('for creating a merge request', () => {
- expect(trackingSpy).toHaveBeenCalledWith(
- document.body.dataset.page,
- TRACKING_ACTION_CREATE_MERGE_REQUEST,
- );
- });
- });
-
- describe('sends the correct Service Ping tracking event', () => {
- beforeEach(() => {
- jest.spyOn(Api, 'trackRedisCounterEvent').mockResolvedValue({ data: '' });
- });
-
- it('for commiting changes', () => {
- return submitContentChanges(buildPayload()).then(() => {
- expect(Api.trackRedisCounterEvent).toHaveBeenCalledWith(
- SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
- );
- });
- });
-
- it('for creating a merge request', () => {
- return submitContentChanges(buildPayload()).then(() => {
- expect(Api.trackRedisCounterEvent).toHaveBeenCalledWith(
- SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/services/templater_spec.js b/spec/frontend/static_site_editor/services/templater_spec.js
deleted file mode 100644
index cb3a0a0c106..00000000000
--- a/spec/frontend/static_site_editor/services/templater_spec.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/* eslint-disable no-useless-escape */
-import templater from '~/static_site_editor/services/templater';
-
-describe('templater', () => {
- const source = `Below this line is a simple ERB (single-line erb block) example.
-
-<% some erb code %>
-
-Below this line is a complex ERB (multi-line erb block) example.
-
-<% if apptype.maturity && (apptype.maturity != "planned") %>
- <% maturity = "This application type is at the \"#{apptype.maturity}\" level of maturity." %>
-<% end %>
-
-Below this line is a non-erb (single-line HTML) markup example that also has erb.
-
-<a href="<%= compensation_roadmap.role_path %>"><%= compensation_roadmap.role_path %></a>
-
-Below this line is a non-erb (multi-line HTML block) markup example that also has erb.
-
-<ul>
-<% compensation_roadmap.recommendation.recommendations.each do |recommendation| %>
- <li><%= recommendation %></li>
-<% end %>
-</ul>
-
-Below this line is a block of HTML.
-
-<div>
- <h1>Heading</h1>
- <p>Some paragraph...</p>
-</div>
-
-Below this line is a codeblock of the same HTML that should be ignored and preserved.
-
-\`\`\` html
-<div>
- <h1>Heading</h1>
- <p>Some paragraph...</p>
-</div>
-\`\`\`
-
-Below this line is a iframe that should be ignored and preserved
-
-<iframe></iframe>
-`;
- const sourceTemplated = `Below this line is a simple ERB (single-line erb block) example.
-
-\`\`\` sse
-<% some erb code %>
-\`\`\`
-
-Below this line is a complex ERB (multi-line erb block) example.
-
-\`\`\` sse
-<% if apptype.maturity && (apptype.maturity != "planned") %>
- <% maturity = "This application type is at the \"#{apptype.maturity}\" level of maturity." %>
-<% end %>
-\`\`\`
-
-Below this line is a non-erb (single-line HTML) markup example that also has erb.
-
-\`\`\` sse
-<a href="<%= compensation_roadmap.role_path %>"><%= compensation_roadmap.role_path %></a>
-\`\`\`
-
-Below this line is a non-erb (multi-line HTML block) markup example that also has erb.
-
-\`\`\` sse
-<ul>
-<% compensation_roadmap.recommendation.recommendations.each do |recommendation| %>
- <li><%= recommendation %></li>
-<% end %>
-</ul>
-\`\`\`
-
-Below this line is a block of HTML.
-
-\`\`\` sse
-<div>
- <h1>Heading</h1>
- <p>Some paragraph...</p>
-</div>
-\`\`\`
-
-Below this line is a codeblock of the same HTML that should be ignored and preserved.
-
-\`\`\` html
-<div>
- <h1>Heading</h1>
- <p>Some paragraph...</p>
-</div>
-\`\`\`
-
-Below this line is a iframe that should be ignored and preserved
-
-<iframe></iframe>
-`;
-
- it.each`
- fn | initial | target
- ${'wrap'} | ${source} | ${sourceTemplated}
- ${'wrap'} | ${sourceTemplated} | ${sourceTemplated}
- ${'unwrap'} | ${sourceTemplated} | ${source}
- ${'unwrap'} | ${source} | ${source}
- `(
- 'wraps $initial in a templated sse codeblocks if $fn is wrap, unwraps otherwise',
- ({ fn, initial, target }) => {
- expect(templater[fn](initial)).toMatch(target);
- },
- );
-});
diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js
new file mode 100644
index 00000000000..b1726a2c0ef
--- /dev/null
+++ b/spec/frontend/tags/components/delete_tag_modal_spec.js
@@ -0,0 +1,138 @@
+import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeleteTagModal from '~/tags/components/delete_tag_modal.vue';
+import eventHub from '~/tags/event_hub';
+
+let wrapper;
+
+const tagName = 'test-tag';
+const path = '/path/to/tag';
+const isProtected = false;
+
+const createComponent = (data = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(DeleteTagModal, {
+ data() {
+ return {
+ tagName,
+ path,
+ isProtected,
+ ...data,
+ };
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+ },
+ }),
+ );
+};
+
+const findModal = () => wrapper.findComponent(GlModal);
+const findModalMessage = () => wrapper.findByTestId('modal-message');
+const findDeleteButton = () => wrapper.findByTestId('delete-tag-confirmation-button');
+const findCancelButton = () => wrapper.findByTestId('delete-tag-cancel-button');
+const findFormInput = () => wrapper.findComponent(GlFormInput);
+const findForm = () => wrapper.find('form');
+
+describe('Delete tag modal', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Deleting a regular tag', () => {
+ const expectedTitle = 'Delete tag. Are you ABSOLUTELY SURE?';
+ const expectedMessage = "You're about to permanently delete the tag test-tag.";
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the modal correctly', () => {
+ expect(findModal().props('title')).toBe(expectedTitle);
+ expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessage);
+ expect(findCancelButton().text()).toBe('Cancel, keep tag');
+ expect(findDeleteButton().text()).toBe('Yes, delete tag');
+ expect(findForm().attributes('action')).toBe(path);
+ });
+
+ it('submits the form when the delete button is clicked', () => {
+ const submitFormSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
+
+ findDeleteButton().trigger('click');
+
+ expect(findForm().attributes('action')).toBe(path);
+ expect(submitFormSpy).toHaveBeenCalled();
+ });
+
+ it('calls show on the modal when a `openModal` event is received through the event hub', async () => {
+ const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
+
+ eventHub.$emit('openModal', {
+ isProtected,
+ tagName,
+ path,
+ });
+
+ expect(showSpy).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();
+ });
+ });
+
+ describe('Deleting a protected tag (for owner or maintainer)', () => {
+ const expectedTitleProtected = 'Delete protected tag. Are you ABSOLUTELY SURE?';
+ const expectedMessageProtected =
+ "You're about to permanently delete the protected tag test-tag.";
+ const expectedConfirmationText =
+ 'After you confirm and select Yes, delete protected tag, you cannot recover this tag. Please type the following to confirm: test-tag';
+
+ beforeEach(() => {
+ createComponent({ isProtected: true });
+ });
+
+ describe('rendering the modal correctly for a protected tag', () => {
+ it('sets the modal title for a protected tag', () => {
+ expect(findModal().props('title')).toBe(expectedTitleProtected);
+ });
+
+ it('renders the correct text in the modal message', () => {
+ expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessageProtected);
+ });
+
+ it('renders the protected tag name confirmation form with expected text and action', () => {
+ expect(findForm().text()).toMatchInterpolatedText(expectedConfirmationText);
+ expect(findForm().attributes('action')).toBe(path);
+ });
+
+ it('renders the buttons with the correct button text', () => {
+ expect(findCancelButton().text()).toBe('Cancel, keep tag');
+ expect(findDeleteButton().text()).toBe('Yes, delete protected tag');
+ });
+ });
+
+ it('opens with the delete button disabled and enables it when tag name is confirmed', async () => {
+ expect(findDeleteButton().props('disabled')).toBe(true);
+
+ findFormInput().vm.$emit('input', tagName);
+
+ await waitForPromises();
+
+ expect(findDeleteButton().props('disabled')).not.toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/tags/init_delete_tag_modal_spec.js b/spec/frontend/tags/init_delete_tag_modal_spec.js
new file mode 100644
index 00000000000..537df4fac52
--- /dev/null
+++ b/spec/frontend/tags/init_delete_tag_modal_spec.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import initDeleteTagModal from '../../../app/assets/javascripts/tags/init_delete_tag_modal';
+
+describe('initDeleteTagModal', () => {
+ beforeEach(() => {
+ setHTMLFixture('<div class="js-delete-tag-modal"></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('should mount the delete tag modal', () => {
+ expect(initDeleteTagModal()).toBeInstanceOf(Vue);
+ expect(document.querySelector('.js-delete-tag-modal')).toBeNull();
+ });
+
+ it('should return false if the mounting element is missing', () => {
+ document.querySelector('.js-delete-tag-modal').remove();
+ expect(initDeleteTagModal()).toBe(false);
+ });
+});
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index d01f6af9023..40b7448d78d 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -69,6 +69,7 @@ describe('StatesTableActions', () => {
wrapper = shallowMount(StateActions, {
apolloProvider,
propsData,
+ provide: { projectPath: 'path/to/project' },
mocks: { $toast: { show: toast } },
stubs: { GlDropdown, GlModal, GlSprintf },
});
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index fa9c8320b4f..16ffd2b7013 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -2,6 +2,8 @@ import { GlBadge, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StatesTable from '~/terraform/components/states_table.vue';
import StateActions from '~/terraform/components/states_table_actions.vue';
@@ -104,11 +106,31 @@ describe('StatesTable', () => {
updatedAt: '2020-10-10T00:00:00Z',
latestVersion: null,
},
+ {
+ _showDetails: false,
+ errorMessages: [],
+ name: 'state-6',
+ loadingLock: false,
+ loadingRemove: false,
+ lockedAt: null,
+ lockedByUser: null,
+ updatedAt: '2020-10-10T00:00:00Z',
+ deletedAt: '2022-02-02T00:00:00Z',
+ latestVersion: null,
+ },
],
};
const createComponent = async (propsData = defaultProps) => {
- wrapper = mount(StatesTable, { propsData });
+ wrapper = extendedWrapper(
+ mount(StatesTable, {
+ propsData,
+ provide: { projectPath: 'path/to/project' },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ }),
+ );
await nextTick();
};
@@ -124,27 +146,28 @@ describe('StatesTable', () => {
});
it.each`
- name | toolTipText | locked | loading | lineNumber
+ name | toolTipText | hasBadge | loading | lineNumber
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0}
${'state-2'} | ${'Locking state'} | ${false} | ${true} | ${1}
${'state-3'} | ${'Unlocking state'} | ${false} | ${true} | ${2}
${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${false} | ${3}
${'state-5'} | ${'Removing'} | ${false} | ${true} | ${4}
+ ${'state-6'} | ${'Deletion in progress'} | ${true} | ${false} | ${5}
`(
'displays the name and locked information "$name" for line "$lineNumber"',
- ({ name, toolTipText, locked, loading, lineNumber }) => {
+ ({ name, toolTipText, hasBadge, loading, lineNumber }) => {
const states = wrapper.findAll('[data-testid="terraform-states-table-name"]');
-
const state = states.at(lineNumber);
- const toolTip = state.find(GlTooltip);
expect(state.text()).toContain(name);
- expect(state.find(GlBadge).exists()).toBe(locked);
+ expect(state.find(GlBadge).exists()).toBe(hasBadge);
expect(state.find(GlLoadingIcon).exists()).toBe(loading);
- expect(toolTip.exists()).toBe(locked);
- if (locked) {
- expect(toolTip.text()).toMatchInterpolatedText(toolTipText);
+ if (hasBadge) {
+ const badge = wrapper.findByTestId(`state-badge-${name}`);
+
+ expect(getBinding(badge.element, 'gl-tooltip')).toBeDefined();
+ expect(badge.attributes('title')).toMatchInterpolatedText(toolTipText);
}
},
);
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index c8b4cd564d9..cfd82768098 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -16,6 +16,9 @@ describe('TerraformList', () => {
const propsData = {
emptyStateImage: '/path/to/image',
+ };
+
+ const provide = {
projectPath: 'path/to/project',
};
@@ -47,6 +50,7 @@ describe('TerraformList', () => {
wrapper = shallowMount(TerraformList, {
apolloProvider,
propsData,
+ provide,
stubs: {
GlTab,
},
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index fa598716645..1544fed5240 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -22,16 +22,17 @@ describe('User Popovers', () => {
const link = document.createElement('a');
link.classList.add('js-user-link');
- link.setAttribute('data-user', '1');
+ link.dataset.user = '1';
return link;
};
+ const findPopovers = () => {
+ return Array.from(document.querySelectorAll('[data-testid="user-popover"]'));
+ };
const dummyUser = { name: 'root', username: 'root', is_followed: false };
const dummyUserStatus = { message: 'active' };
- let popovers;
-
const triggerEvent = (eventName, el) => {
const event = new MouseEvent(eventName, {
bubbles: true,
@@ -54,56 +55,73 @@ describe('User Popovers', () => {
.mockImplementation((userId) => userStatusCacheSpy(userId));
jest.spyOn(UsersCache, 'updateById');
- popovers = initUserPopovers(document.querySelectorAll(selector));
+ initUserPopovers((popoverInstance) => {
+ const mountingRoot = document.createElement('div');
+ document.body.appendChild(mountingRoot);
+ popoverInstance.$mount(mountingRoot);
+ });
});
afterEach(() => {
resetHTMLFixture();
});
- it('initializes a popover for each user link with a user id', () => {
- const linksWithUsers = findFixtureLinks();
+ describe('shows a placeholder popover on hover', () => {
+ let linksWithUsers;
+ beforeEach(() => {
+ linksWithUsers = findFixtureLinks();
+ linksWithUsers.forEach((el) => {
+ triggerEvent('mouseover', el);
+ });
+ });
- expect(linksWithUsers.length).toBe(popovers.length);
- });
+ it('for initial links', () => {
+ expect(findPopovers().length).toBe(linksWithUsers.length);
+ });
- it('adds popovers to user links added to the DOM tree after the initial call', async () => {
- document.body.appendChild(createUserLink());
- document.body.appendChild(createUserLink());
+ it('for elements added after initial load', async () => {
+ const addedLinks = [createUserLink(), createUserLink()];
+ addedLinks.forEach((link) => {
+ document.body.appendChild(link);
+ });
- const linksWithUsers = findFixtureLinks();
+ jest.runOnlyPendingTimers();
- expect(linksWithUsers.length).toBe(popovers.length + 2);
+ addedLinks.forEach((link) => {
+ triggerEvent('mouseover', link);
+ });
+
+ expect(findPopovers().length).toBe(linksWithUsers.length + addedLinks.length);
+ });
});
- it('does not initialize the user popovers twice for the same element', () => {
- const newPopovers = initUserPopovers(document.querySelectorAll(selector));
- const samePopovers = popovers.every((popover, index) => newPopovers[index] === popover);
+ it('does not initialize the user popovers twice for the same element', async () => {
+ const [firstUserLink] = findFixtureLinks();
+ triggerEvent('mouseover', firstUserLink);
+ jest.runOnlyPendingTimers();
+ triggerEvent('mouseleave', firstUserLink);
+ jest.runOnlyPendingTimers();
+ triggerEvent('mouseover', firstUserLink);
+ jest.runOnlyPendingTimers();
- expect(samePopovers).toBe(true);
+ expect(findPopovers().length).toBe(1);
});
- describe('when user link emits mouseenter event', () => {
+ describe('when user link emits mouseenter event with empty user cache', () => {
let userLink;
beforeEach(() => {
UsersCache.retrieveById.mockReset();
- userLink = document.querySelector(selector);
-
- triggerEvent('mouseenter', userLink);
- });
+ [userLink] = findFixtureLinks();
- it('removes title attribute from user links', () => {
- expect(userLink.getAttribute('title')).toBeFalsy();
- expect(userLink.dataset.originalTitle).toBeFalsy();
+ triggerEvent('mouseover', userLink);
});
- it('populates popovers with preloaded user data', () => {
+ it('populates popover with preloaded user data', () => {
const { name, userId, username } = userLink.dataset;
- const [firstPopover] = popovers;
- expect(firstPopover.$props.user).toEqual(
+ expect(userLink.user).toEqual(
expect.objectContaining({
name,
userId,
@@ -111,6 +129,21 @@ describe('User Popovers', () => {
}),
);
});
+ });
+
+ describe('when user link emits mouseenter event', () => {
+ let userLink;
+
+ beforeEach(() => {
+ [userLink] = findFixtureLinks();
+
+ triggerEvent('mouseover', userLink);
+ });
+
+ it('removes title attribute from user links', () => {
+ expect(userLink.getAttribute('title')).toBeFalsy();
+ expect(userLink.dataset.originalTitle).toBeFalsy();
+ });
it('fetches user info and status from the user cache', () => {
const { userId } = userLink.dataset;
@@ -118,42 +151,38 @@ describe('User Popovers', () => {
expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId);
expect(UsersCache.retrieveStatusById).toHaveBeenCalledWith(userId);
});
- });
-
- it('removes aria-describedby attribute from the user link on mouseleave', () => {
- const userLink = document.querySelector(selector);
- userLink.setAttribute('aria-describedby', 'popover');
- triggerEvent('mouseleave', userLink);
+ it('removes aria-describedby attribute from the user link on mouseleave', () => {
+ userLink.setAttribute('aria-describedby', 'popover');
+ triggerEvent('mouseleave', userLink);
- expect(userLink.getAttribute('aria-describedby')).toBe(null);
- });
-
- it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => {
- const [firstPopover] = popovers;
- const withinFirstPopover = within(firstPopover.$el);
- const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' });
- const findUnfollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Unfollow' });
+ expect(userLink.getAttribute('aria-describedby')).toBe(null);
+ });
- const userLink = document.querySelector(selector);
- triggerEvent('mouseenter', userLink);
+ it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => {
+ const [firstPopover] = findPopovers();
+ const withinFirstPopover = within(firstPopover);
+ const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' });
+ const findUnfollowButton = () =>
+ withinFirstPopover.queryByRole('button', { name: 'Unfollow' });
- await waitForPromises();
+ jest.runOnlyPendingTimers();
- const { userId } = document.querySelector(selector).dataset;
+ const { userId } = document.querySelector(selector).dataset;
- triggerEvent('click', findFollowButton());
+ triggerEvent('click', findFollowButton());
- await waitForPromises();
+ await waitForPromises();
- expect(findUnfollowButton()).not.toBe(null);
- expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true });
+ expect(findUnfollowButton()).not.toBe(null);
+ expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true });
- triggerEvent('click', findUnfollowButton());
+ triggerEvent('click', findUnfollowButton());
- await waitForPromises();
+ await waitForPromises();
- expect(findFollowButton()).not.toBe(null);
- expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false });
+ expect(findFollowButton()).not.toBe(null);
+ expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false });
+ });
});
});
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 59edde48eab..9231e38ea90 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -95,10 +95,10 @@ export const setAssignees = (...users) => {
const input = document.createElement('input');
input.name = 'merge_request[assignee_ids][]';
input.value = user.id.toString();
- input.setAttribute('data-avatar-url', user.avatar_url);
- input.setAttribute('data-name', user.name);
- input.setAttribute('data-username', user.username);
- input.setAttribute('data-can-merge', user.can_merge);
+ input.dataset.avatarUrl = user.avatar_url;
+ input.dataset.name = user.name;
+ input.dataset.username = user.username;
+ input.dataset.canMerge = user.can_merge;
return input;
}),
);
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
index 4985417ad99..05cd1bb5b3d 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createFlash from '~/flash';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
@@ -15,6 +15,7 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
jest.mock('~/flash');
+const RULE_NAME = 'first_rule';
const TEST_HELP_PATH = 'help/path';
const testApprovedBy = () => [1, 7, 10].map((id) => ({ id }));
const testApprovals = () => ({
@@ -26,6 +27,7 @@ const testApprovals = () => ({
user_can_approve: true,
user_has_approved: true,
require_password_to_approve: false,
+ invalid_approvers_rules: [],
});
const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] });
@@ -41,6 +43,9 @@ describe('MRWidget approvals', () => {
service,
...props,
},
+ stubs: {
+ GlSprintf,
+ },
});
};
@@ -58,6 +63,7 @@ describe('MRWidget approvals', () => {
};
const findSummary = () => wrapper.find(ApprovalsSummary);
const findOptionalSummary = () => wrapper.find(ApprovalsSummaryOptional);
+ const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]');
beforeEach(() => {
service = {
@@ -171,7 +177,7 @@ describe('MRWidget approvals', () => {
it('approve action is rendered', () => {
expect(findActionData()).toEqual({
- variant: 'info',
+ variant: 'confirm',
text: 'Approve',
category: 'primary',
});
@@ -192,7 +198,7 @@ describe('MRWidget approvals', () => {
it('approve action (with inverted style) is rendered', () => {
expect(findActionData()).toEqual({
- variant: 'info',
+ variant: 'confirm',
text: 'Approve',
category: 'secondary',
});
@@ -208,7 +214,7 @@ describe('MRWidget approvals', () => {
it('approve additionally action is rendered', () => {
expect(findActionData()).toEqual({
- variant: 'info',
+ variant: 'confirm',
text: 'Approve additionally',
category: 'secondary',
});
@@ -279,9 +285,9 @@ describe('MRWidget approvals', () => {
it('revoke action is rendered', () => {
expect(findActionData()).toEqual({
- variant: 'warning',
+ category: 'primary',
+ variant: 'default',
text: 'Revoke approval',
- category: 'secondary',
});
});
@@ -383,4 +389,36 @@ describe('MRWidget approvals', () => {
});
});
});
+
+ describe('invalid rules', () => {
+ beforeEach(() => {
+ mr.approvals.merge_request_approvers_available = true;
+ createComponent();
+ });
+
+ it('does not render related components', () => {
+ expect(findInvalidRules().exists()).toBe(false);
+ });
+
+ describe('when invalid rules are present', () => {
+ beforeEach(() => {
+ mr.approvals.invalid_approvers_rules = [{ name: RULE_NAME }];
+ createComponent();
+ });
+
+ it('renders related components', () => {
+ const invalidRules = findInvalidRules();
+
+ expect(invalidRules.exists()).toBe(true);
+
+ const invalidRulesText = invalidRules.text();
+
+ expect(invalidRulesText).toContain(RULE_NAME);
+ expect(invalidRulesText).toContain(
+ 'GitLab has approved this rule automatically to unblock the merge request.',
+ );
+ expect(invalidRulesText).toContain('Learn more.');
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js b/spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js
new file mode 100644
index 00000000000..d6776c00b29
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/approvals/humanized_text_spec.js
@@ -0,0 +1,18 @@
+import { humanizeInvalidApproversRules } from '~/vue_merge_request_widget/components/approvals/humanized_text';
+
+const testRules = [{ name: 'Lorem' }, { name: 'Ipsum' }, { name: 'Dolar' }];
+
+describe('humanizeInvalidApproversRules', () => {
+ it('returns text in regards to a single rule', () => {
+ const [singleRule] = testRules;
+ expect(humanizeInvalidApproversRules([singleRule])).toBe('"Lorem"');
+ });
+
+ it('returns empty text when there is no rule', () => {
+ expect(humanizeInvalidApproversRules([])).toBe('');
+ });
+
+ it('returns text in regards to multiple rules', () => {
+ expect(humanizeInvalidApproversRules(testRules)).toBe('"Lorem", "Ipsum" and "Dolar"');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
index 63df63a9b00..dc25596655a 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
+++ b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
@@ -21,8 +21,8 @@ describe('MR widget extension registering', () => {
expect.objectContaining({
extends: ExtensionBase,
name: 'Test',
- props: ['helloWorld'],
computed: {
+ helloWorld: expect.any(Function),
test: expect.any(Function),
},
methods: {
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
index 82526af7afa..01fbcb2154f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
@@ -42,8 +42,8 @@ describe('Merge Request Collapsible Extension', () => {
expect(wrapper.find('[data-testid="collapsed-header"]').text()).toBe('hello there');
});
- it('renders angle-right icon', () => {
- expect(findIcon().props('name')).toBe('angle-right');
+ it('renders chevron-lg-right icon', () => {
+ expect(findIcon().props('name')).toBe('chevron-lg-right');
});
describe('onClick', () => {
@@ -60,8 +60,8 @@ describe('Merge Request Collapsible Extension', () => {
expect(findTitle().text()).toBe('Collapse');
});
- it('renders angle-down icon', () => {
- expect(findIcon().props('name')).toBe('angle-down');
+ it('renders chevron-lg-down icon', () => {
+ expect(findIcon().props('name')).toBe('chevron-lg-down');
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
deleted file mode 100644
index ed6dc598845..00000000000
--- a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js
+++ /dev/null
@@ -1,176 +0,0 @@
-import { shallowMount, mount } from '@vue/test-utils';
-import Header from '~/vue_merge_request_widget/components/mr_widget_header.vue';
-
-describe('MRWidgetHeader', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(Header, {
- propsData,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- gon.relative_url_root = '';
- });
-
- const commonMrProps = {
- divergedCommitsCount: 1,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'main',
- targetBranchPath: '/foo/bar/main',
- statusPath: 'abc',
- };
-
- describe('computed', () => {
- describe('shouldShowCommitsBehindText', () => {
- it('return true when there are divergedCommitsCount', () => {
- createComponent({
- mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'main',
- statusPath: 'abc',
- },
- });
-
- expect(wrapper.vm.shouldShowCommitsBehindText).toBe(true);
- });
-
- it('returns false where there are no divergedComits count', () => {
- createComponent({
- mr: {
- divergedCommitsCount: 0,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
- targetBranch: 'main',
- statusPath: 'abc',
- },
- });
-
- expect(wrapper.vm.shouldShowCommitsBehindText).toBe(false);
- });
- });
-
- describe('commitsBehindText', () => {
- it('returns singular when there is one commit', () => {
- wrapper = mount(Header, {
- propsData: {
- mr: commonMrProps,
- },
- });
-
- expect(wrapper.find('.diverged-commits-count').element.innerHTML).toBe(
- 'The source branch is <a href="/foo/bar/main" class="gl-link">1 commit behind</a> the target branch',
- );
- });
-
- it('returns plural when there is more than one commit', () => {
- wrapper = mount(Header, {
- propsData: {
- mr: {
- ...commonMrProps,
- divergedCommitsCount: 2,
- },
- },
- });
- expect(wrapper.find('.diverged-commits-count').element.innerHTML).toBe(
- 'The source branch is <a href="/foo/bar/main" class="gl-link">2 commits behind</a> the target branch',
- );
- });
- });
- });
-
- describe('template', () => {
- describe('common elements', () => {
- beforeEach(() => {
- createComponent({
- mr: {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'main',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- statusPath: 'abc',
- },
- });
- });
-
- it('renders source branch link', () => {
- expect(wrapper.find('.js-source-branch').html()).toContain(
- '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- );
- });
-
- it('renders clipboard button', () => {
- expect(wrapper.find('[data-testid="mr-widget-copy-clipboard"]')).not.toBe(null);
- });
-
- it('renders target branch', () => {
- expect(wrapper.find('.js-target-branch').text().trim()).toBe('main');
- });
- });
-
- describe('without diverged commits', () => {
- beforeEach(() => {
- createComponent({
- mr: {
- divergedCommitsCount: 0,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'main',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- statusPath: 'abc',
- },
- });
- });
-
- it('does not render diverged commits info', () => {
- expect(wrapper.find('.diverged-commits-count').exists()).toBe(false);
- });
- });
-
- describe('with diverged commits', () => {
- beforeEach(() => {
- wrapper = mount(Header, {
- propsData: {
- mr: {
- ...commonMrProps,
- divergedCommitsCount: 12,
- sourceBranchRemoved: false,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- },
- },
- });
- });
-
- it('renders diverged commits info', () => {
- expect(wrapper.find('.diverged-commits-count').text().trim()).toBe(
- 'The source branch is 12 commits behind the target branch',
- );
-
- expect(wrapper.find('.diverged-commits-count a').text().trim()).toBe('12 commits behind');
- expect(wrapper.find('.diverged-commits-count a').attributes('href')).toBe(
- wrapper.vm.mr.targetBranchPath,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
index 8e710b6d65f..352bc1a08ea 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -71,7 +71,7 @@ describe('MRWidgetSuggestPipeline', () => {
const button = findOkBtn();
expect(button.exists()).toBe(true);
- expect(button.classes('btn-info')).toEqual(true);
+ expect(button.classes('btn-confirm')).toEqual(true);
expect(button.attributes('href')).toBe(suggestProps.pipelinePath);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 8efc4d84624..29ee7e0010f 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -193,9 +193,7 @@ describe('MRWidgetMerged', () => {
it('shows button to copy commit SHA to clipboard', () => {
expect(selectors.copyMergeShaButton).not.toBe(null);
- expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(
- vm.mr.mergeCommitSha,
- );
+ expect(selectors.copyMergeShaButton.dataset.clipboardText).toBe(vm.mr.mergeCommitSha);
});
it('hides button to copy commit SHA if SHA does not exist', async () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index da3a323e8ea..46d90ddc83c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -87,7 +87,11 @@ const createReadyToMergeResponse = (customMr) => {
});
};
-const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) => {
+const createComponent = (
+ customConfig = {},
+ mergeRequestWidgetGraphql = false,
+ restructuredMrWidget = false,
+) => {
wrapper = shallowMount(ReadyToMerge, {
localVue,
propsData: {
@@ -97,6 +101,7 @@ const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) =
provide: {
glFeatures: {
mergeRequestWidgetGraphql,
+ restructuredMrWidget,
},
},
stubs: {
@@ -307,6 +312,20 @@ describe('ReadyToMerge', () => {
},
});
+ beforeEach(() => {
+ readyToMergeResponseSpy = jest
+ .fn()
+ .mockResolvedValueOnce(createReadyToMergeResponse({ squash: true, squashOnMerge: true }))
+ .mockResolvedValue(
+ createReadyToMergeResponse({
+ squash: true,
+ squashOnMerge: true,
+ defaultMergeCommitMessage: '',
+ defaultSquashCommitMessage: '',
+ }),
+ );
+ });
+
it('should handle merge when pipeline succeeds', async () => {
createComponent();
@@ -379,6 +398,27 @@ describe('ReadyToMerge', () => {
expect(params.should_remove_source_branch).toBeTruthy();
expect(params.auto_merge_strategy).toBeUndefined();
});
+
+ it('hides edit commit message', async () => {
+ createComponent({}, true, true);
+
+ await waitForPromises();
+
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('success'));
+
+ await wrapper
+ .findComponent('[data-testid="widget_edit_commit_message"]')
+ .vm.$emit('input', true);
+
+ expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(true);
+
+ wrapper.vm.handleMergeButtonClick();
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(false);
+ });
});
describe('initiateRemoveSourceBranchPolling', () => {
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index b7c22b403aa..8f20d6a8fc9 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -51,7 +51,7 @@ describe('MrWidgetTerraformConainer', () => {
});
it('diplays loading skeleton', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(false);
});
});
@@ -63,7 +63,7 @@ describe('MrWidgetTerraformConainer', () => {
});
it('displays terraform content', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
expect(wrapper.find(MrWidgetExpanableSection).exists()).toBe(true);
expect(findPlans()).toEqual(Object.values(plans));
});
@@ -158,7 +158,7 @@ describe('MrWidgetTerraformConainer', () => {
});
it('stops loading', () => {
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
});
it('generates one broken plan', () => {
diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
index 2bc6860743a..da4b990c078 100644
--- a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
@@ -9,6 +9,7 @@ import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import httpStatusCodes from '~/lib/utils/http_status';
+import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import { failedReport } from 'jest/reports/mock_data/mock_data';
import mixedResultsTestReports from 'jest/reports/mock_data/new_and_fixed_failures_report.json';
@@ -39,6 +40,7 @@ describe('Test report extension', () => {
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
const findTertiaryButton = () => wrapper.find(GlButton);
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
+ const findModal = () => wrapper.find(TestCaseDetails);
const createComponent = () => {
wrapper = mountExtended(extensionsContainer, {
@@ -190,4 +192,19 @@ describe('Test report extension', () => {
);
});
});
+
+ describe('modal link', () => {
+ beforeEach(async () => {
+ await createExpandedWidgetWithData();
+
+ wrapper.findByTestId('modal-link').trigger('click');
+ });
+
+ it('opens a modal to display test case details', () => {
+ expect(findModal().exists()).toBe(true);
+ expect(findModal().props('testCase')).toMatchObject(
+ mixedResultsTestReports.suites[0].new_failures[0],
+ );
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
index f8ea6fc23a2..77b3576a3d3 100644
--- a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
+++ b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
@@ -14,6 +15,8 @@ import {
invalidPlanWithoutName,
} from '../../components/terraform/mock_data';
+jest.mock('~/api.js');
+
describe('Terraform extension', () => {
let wrapper;
let mock;
@@ -130,20 +133,33 @@ describe('Terraform extension', () => {
}
});
});
+
+ it('responds with the correct telemetry when the deeply nested "Full log" link is clicked', () => {
+ api.trackRedisHllUserEvent.mockClear();
+ api.trackRedisCounterEvent.mockClear();
+
+ findListItem(0).find('[data-testid="extension-actions-button"]').trigger('click');
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_terraform_click_full_report',
+ );
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledTimes(1);
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_terraform_count_click_full_report',
+ );
+ });
});
describe('polling', () => {
let pollRequest;
- let pollStop;
beforeEach(() => {
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
- pollStop = jest.spyOn(Poll.prototype, 'stop');
});
afterEach(() => {
pollRequest.mockRestore();
- pollStop.mockRestore();
});
describe('successful poll', () => {
@@ -155,7 +171,6 @@ describe('Terraform extension', () => {
it('does not make additional requests after poll is successful', () => {
expect(pollRequest).toHaveBeenCalledTimes(1);
- expect(pollStop).toHaveBeenCalledTimes(1);
});
});
@@ -171,7 +186,6 @@ describe('Terraform extension', () => {
it('does not make additional requests after poll is unsuccessful', () => {
expect(pollRequest).toHaveBeenCalledTimes(1);
- expect(pollStop).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 9719e81fe12..6abbb052aef 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -29,8 +29,11 @@ import {
workingExtension,
collapsedDataErrorExtension,
fullDataErrorExtension,
+ fullReportExtension,
+ noTelemetryExtension,
pollingExtension,
pollingErrorExtension,
+ multiPollingExtension,
} from './test_extensions';
jest.mock('~/api.js');
@@ -48,6 +51,8 @@ describe('MrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
const findExtensionToggleButton = () =>
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
+ const findExtensionLink = (linkHref) =>
+ wrapper.find(`[data-testid="widget-extension"] [href="${linkHref}"]`);
beforeEach(() => {
gl.mrWidgetData = { ...mockData };
@@ -67,7 +72,7 @@ describe('MrWidgetOptions', () => {
gon.features = {};
});
- const createComponent = (mrData = mockData, options = {}) => {
+ const createComponent = (mrData = mockData, options = {}, glFeatures = {}) => {
if (wrapper) {
wrapper.destroy();
}
@@ -76,6 +81,9 @@ describe('MrWidgetOptions', () => {
propsData: {
mrData: { ...mrData },
},
+ provide: {
+ glFeatures,
+ },
...options,
});
@@ -423,7 +431,7 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
- favicon.setAttribute('data-original-href', faviconDataUrl);
+ favicon.dataset.originalHref = faviconDataUrl;
document.body.appendChild(favicon);
faviconElement = document.getElementById('favicon');
@@ -621,7 +629,16 @@ describe('MrWidgetOptions', () => {
});
describe('code quality widget', () => {
- it('renders the component', () => {
+ 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);
});
});
@@ -911,18 +928,6 @@ describe('MrWidgetOptions', () => {
expect(wrapper.text()).toContain('Test extension summary count: 1');
});
- it('triggers trackRedisHllUserEvent API call', async () => {
- await waitForPromises();
-
- wrapper
- .find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
- .trigger('click');
-
- await nextTick();
-
- expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_expand_event');
- });
-
it('renders full data', async () => {
await waitForPromises();
@@ -982,31 +987,98 @@ describe('MrWidgetOptions', () => {
describe('mock polling extension', () => {
let pollRequest;
- let pollStop;
+
+ const findWidgetTestExtension = () => wrapper.find('[data-testid="widget-extension"]');
beforeEach(() => {
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
- pollStop = jest.spyOn(Poll.prototype, 'stop');
+
+ registeredExtensions.extensions = [];
});
afterEach(() => {
pollRequest.mockRestore();
- pollStop.mockRestore();
registeredExtensions.extensions = [];
+
+ // Clear all left-over timeouts that may be registered in the poll class
+ let id = window.setTimeout(() => {}, 0);
+
+ while (id > 0) {
+ window.clearTimeout(id);
+ id -= 1;
+ }
});
- describe('success', () => {
- beforeEach(() => {
- registerExtension(pollingExtension);
+ describe('success - multi polling', () => {
+ it('sets data when polling is complete', async () => {
+ registerExtension(
+ multiPollingExtension([
+ () =>
+ Promise.resolve({
+ headers: { 'poll-interval': 0 },
+ status: 200,
+ data: { reports: 'parsed' },
+ }),
+ () =>
+ Promise.resolve({
+ status: 200,
+ data: { reports: 'parsed' },
+ }),
+ ]),
+ );
- createComponent();
+ await createComponent();
+ expect(findWidgetTestExtension().html()).toContain(
+ 'Multi polling test extension reports: parsed, count: 2',
+ );
});
- it('does not make additional requests after poll is successful', () => {
+ it('shows loading state until polling is complete', async () => {
+ registerExtension(
+ multiPollingExtension([
+ () =>
+ Promise.resolve({
+ headers: { 'poll-interval': 1 },
+ status: 204,
+ }),
+ () =>
+ Promise.resolve({
+ status: 200,
+ data: { reports: 'parsed' },
+ }),
+ ]),
+ );
+
+ await createComponent();
+ expect(findWidgetTestExtension().html()).toContain('Test extension loading...');
+ });
+ });
+
+ describe('success', () => {
+ it('does not make additional requests after poll is successful', async () => {
+ registerExtension(pollingExtension);
+ await createComponent();
// called two times due to parent component polling (mount) and extension polling
expect(pollRequest).toHaveBeenCalledTimes(2);
- expect(pollStop).toHaveBeenCalledTimes(1);
+ });
+
+ it('keeps polling when poll-interval header is provided', async () => {
+ registerExtension({
+ ...pollingExtension,
+ methods: {
+ ...pollingExtension.methods,
+ fetchCollapsedData() {
+ return Promise.resolve({
+ data: {},
+ headers: { 'poll-interval': 1 },
+ status: 204,
+ });
+ },
+ },
+ });
+ await createComponent();
+ expect(findWidgetTestExtension().html()).toContain('Test extension loading...');
});
});
@@ -1024,7 +1096,6 @@ describe('MrWidgetOptions', () => {
it('does not make additional requests after poll has failed', () => {
// called two times due to parent component polling (mount) and extension polling
expect(pollRequest).toHaveBeenCalledTimes(2);
- expect(pollStop).toHaveBeenCalledTimes(1);
});
it('captures sentry error and displays error when poll has failed', () => {
@@ -1080,4 +1151,119 @@ describe('MrWidgetOptions', () => {
itHandlesTheException();
});
});
+
+ describe('telemetry', () => {
+ afterEach(() => {
+ registeredExtensions.extensions = [];
+ });
+
+ it('triggers view events when mounted', () => {
+ registerExtension(workingExtension());
+ createComponent();
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_view',
+ );
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledTimes(1);
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_count_view',
+ );
+ });
+
+ describe('expand button', () => {
+ it('triggers expand events when clicked', async () => {
+ registerExtension(workingExtension());
+ createComponent();
+
+ await waitForPromises();
+
+ api.trackRedisHllUserEvent.mockClear();
+ api.trackRedisCounterEvent.mockClear();
+
+ findExtensionToggleButton().trigger('click');
+
+ // The default working extension is a "warning" type, which generates a second - more specific - telemetry event for expansions
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(2);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_expand',
+ );
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_expand_warning',
+ );
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledTimes(2);
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_count_expand',
+ );
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_count_expand_warning',
+ );
+ });
+
+ it.each`
+ widgetName | nonStandardEvent
+ ${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'}
+ ${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'}
+ ${'WidgetIssues'} | ${'i_testing_load_performance_widget_total'}
+ ${'WidgetTestReport'} | ${'i_testing_summary_widget_total'}
+ `(
+ "sends non-standard events for the '$widgetName' widget",
+ async ({ widgetName, nonStandardEvent }) => {
+ const definition = {
+ ...workingExtension(),
+ name: widgetName,
+ };
+
+ registerExtension(definition);
+ createComponent();
+
+ await waitForPromises();
+
+ api.trackRedisHllUserEvent.mockClear();
+
+ findExtensionToggleButton().trigger('click');
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(nonStandardEvent);
+ },
+ );
+ });
+
+ it('triggers the "full report clicked" events when the appropriate button is clicked', () => {
+ registerExtension(fullReportExtension);
+ createComponent();
+
+ api.trackRedisHllUserEvent.mockClear();
+ api.trackRedisCounterEvent.mockClear();
+
+ findExtensionLink('testref').trigger('click');
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_click_full_report',
+ );
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledTimes(1);
+ expect(api.trackRedisCounterEvent).toHaveBeenCalledWith(
+ 'i_merge_request_widget_test_extension_count_click_full_report',
+ );
+ });
+
+ describe('when disabled', () => {
+ afterEach(() => {
+ registeredExtensions.extensions = [];
+ });
+
+ it("doesn't emit any telemetry events", async () => {
+ registerExtension(noTelemetryExtension);
+ createComponent();
+
+ await waitForPromises();
+
+ findExtensionToggleButton().trigger('click');
+ findExtensionLink('testref').trigger('click'); // The "full report" link
+
+ expect(api.trackRedisHllUserEvent).not.toHaveBeenCalled();
+ expect(api.trackRedisCounterEvent).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/test_extensions.js b/spec/frontend/vue_mr_widget/test_extensions.js
index 6344636873f..76644e0be77 100644
--- a/spec/frontend/vue_mr_widget/test_extensions.js
+++ b/spec/frontend/vue_mr_widget/test_extensions.js
@@ -4,11 +4,14 @@ export const workingExtension = (shouldCollapse = true) => ({
name: 'WidgetTestExtension',
props: ['targetProjectFullPath'],
expandEvent: 'test_expand_event',
+ i18n: {
+ loading: 'Test extension loading...',
+ },
computed: {
- summary({ count, targetProjectFullPath }) {
+ summary({ count, targetProjectFullPath } = {}) {
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
},
- statusIcon({ count }) {
+ statusIcon({ count } = {}) {
return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
},
shouldCollapse() {
@@ -106,6 +109,50 @@ export const pollingExtension = {
enablePolling: true,
};
+export const fullReportExtension = {
+ ...workingExtension(),
+ computed: {
+ ...workingExtension().computed,
+ tertiaryButtons() {
+ return [
+ {
+ text: 'test',
+ href: `testref`,
+ target: '_blank',
+ fullReport: true,
+ },
+ ];
+ },
+ },
+};
+
+export const noTelemetryExtension = {
+ ...fullReportExtension,
+ telemetry: false,
+};
+
+export const multiPollingExtension = (endpointsToBePolled) => ({
+ name: 'WidgetTestMultiPollingExtension',
+ props: [],
+ i18n: {
+ loading: 'Test extension loading...',
+ },
+ computed: {
+ summary(data) {
+ return `Multi polling test extension reports: ${data?.[0]?.reports}, count: ${data.length}`;
+ },
+ statusIcon(data) {
+ return data?.[0]?.reports === 'parsed' ? EXTENSION_ICONS.success : EXTENSION_ICONS.warning;
+ },
+ },
+ enablePolling: true,
+ methods: {
+ fetchMultiData() {
+ return endpointsToBePolled;
+ },
+ },
+});
+
export const pollingErrorExtension = {
...collapsedDataErrorExtension,
enablePolling: true,
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 7aa54a1c55a..ce51af31a70 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -201,28 +201,6 @@ describe('AlertDetails', () => {
});
});
- describe('Threat Monitoring details', () => {
- it('should not render the metrics tab', () => {
- mountComponent({
- data: { alert: mockAlert },
- provide: { isThreatMonitoringPage: true },
- });
- expect(findMetricsTab().exists()).toBe(false);
- });
-
- it('should display "View incident" button that links the issues page when incident exists', () => {
- const iid = '3';
- mountComponent({
- data: { alert: { ...mockAlert, issue: { iid } }, sidebarStatus: false },
- provide: { isThreatMonitoringPage: true },
- });
-
- expect(findViewIncidentBtn().exists()).toBe(true);
- expect(findViewIncidentBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, iid));
- expect(findCreateIncidentBtn().exists()).toBe(false);
- });
- });
-
describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => {
const iid = '3';
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index 44b4c0398cd..30e15595193 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -12,7 +12,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
right="true"
size="medium"
text="Clone"
- variant="info"
+ variant="confirm"
>
<div
class="pb-2 mx-1"
@@ -24,41 +24,38 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<div
class="mx-3"
>
- <div
- readonly="readonly"
+ <b-input-group-stub
+ readonly=""
+ tag="div"
>
- <b-input-group-stub
+ <!---->
+
+ <b-form-input-stub
+ class="gl-form-input"
+ debounce="0"
+ formatter="[Function]"
+ readonly="true"
+ type="text"
+ value="ssh://foo.bar"
+ />
+
+ <b-input-group-append-stub
tag="div"
>
- <!---->
-
- <b-form-input-stub
- class="gl-form-input"
- debounce="0"
- formatter="[Function]"
- readonly="true"
- type="text"
- value="ssh://foo.bar"
+ <gl-button-stub
+ aria-label="Copy URL"
+ buttontextclasses=""
+ category="primary"
+ class="d-inline-flex"
+ data-clipboard-text="ssh://foo.bar"
+ data-qa-selector="copy_ssh_url_button"
+ icon="copy-to-clipboard"
+ size="medium"
+ title="Copy URL"
+ variant="default"
/>
-
- <b-input-group-append-stub
- tag="div"
- >
- <gl-button-stub
- aria-label="Copy URL"
- buttontextclasses=""
- category="primary"
- class="d-inline-flex"
- data-clipboard-text="ssh://foo.bar"
- data-qa-selector="copy_ssh_url_button"
- icon="copy-to-clipboard"
- size="medium"
- title="Copy URL"
- variant="default"
- />
- </b-input-group-append-stub>
- </b-input-group-stub>
- </div>
+ </b-input-group-append-stub>
+ </b-input-group-stub>
</div>
<gl-dropdown-section-header-stub>
@@ -68,41 +65,38 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<div
class="mx-3"
>
- <div
- readonly="readonly"
+ <b-input-group-stub
+ readonly=""
+ tag="div"
>
- <b-input-group-stub
+ <!---->
+
+ <b-form-input-stub
+ class="gl-form-input"
+ debounce="0"
+ formatter="[Function]"
+ readonly="true"
+ type="text"
+ value="http://foo.bar"
+ />
+
+ <b-input-group-append-stub
tag="div"
>
- <!---->
-
- <b-form-input-stub
- class="gl-form-input"
- debounce="0"
- formatter="[Function]"
- readonly="true"
- type="text"
- value="http://foo.bar"
+ <gl-button-stub
+ aria-label="Copy URL"
+ buttontextclasses=""
+ category="primary"
+ class="d-inline-flex"
+ data-clipboard-text="http://foo.bar"
+ data-qa-selector="copy_http_url_button"
+ icon="copy-to-clipboard"
+ size="medium"
+ title="Copy URL"
+ variant="default"
/>
-
- <b-input-group-append-stub
- tag="div"
- >
- <gl-button-stub
- aria-label="Copy URL"
- buttontextclasses=""
- category="primary"
- class="d-inline-flex"
- data-clipboard-text="http://foo.bar"
- data-qa-selector="copy_http_url_button"
- icon="copy-to-clipboard"
- size="medium"
- title="Copy URL"
- variant="default"
- />
- </b-input-group-append-stub>
- </b-input-group-stub>
- </div>
+ </b-input-group-append-stub>
+ </b-input-group-stub>
</div>
</div>
</gl-dropdown-stub>
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 6d52db7ae65..1b502f9587c 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -5,6 +5,8 @@ import ciIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Icon component', () => {
let wrapper;
+ const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]');
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -23,6 +25,52 @@ describe('CI Icon component', () => {
expect(wrapper.find(GlIcon).exists()).toBe(true);
});
+ describe('active icons', () => {
+ it.each`
+ isActive | cssClass
+ ${true} | ${'active'}
+ ${false} | ${'active'}
+ `('active should be $isActive', ({ isActive, cssClass }) => {
+ wrapper = shallowMount(ciIcon, {
+ propsData: {
+ status: {
+ icon: 'status_success',
+ },
+ isActive,
+ },
+ });
+
+ if (isActive) {
+ expect(findIconWrapper().classes()).toContain(cssClass);
+ } else {
+ expect(findIconWrapper().classes()).not.toContain(cssClass);
+ }
+ });
+ });
+
+ describe('interactive icons', () => {
+ it.each`
+ isInteractive | cssClass
+ ${true} | ${'interactive'}
+ ${false} | ${'interactive'}
+ `('interactive should be $isInteractive', ({ isInteractive, cssClass }) => {
+ wrapper = shallowMount(ciIcon, {
+ propsData: {
+ status: {
+ icon: 'status_success',
+ },
+ isInteractive,
+ },
+ });
+
+ if (isInteractive) {
+ expect(findIconWrapper().classes()).toContain(cssClass);
+ } else {
+ expect(findIconWrapper().classes()).not.toContain(cssClass);
+ }
+ });
+ });
+
describe('rendering a status', () => {
it.each`
icon | group | cssClass
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
new file mode 100644
index 00000000000..fe614f03119
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
@@ -0,0 +1,35 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { hexToRgb } from '~/lib/utils/color_utils';
+import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
+import { color } from './mock_data';
+
+describe('ColorItem', () => {
+ let wrapper;
+
+ const propsData = color;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ColorItem, {
+ propsData,
+ });
+ };
+
+ const findColorItem = () => wrapper.findByTestId('color-item');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the correct title', () => {
+ expect(wrapper.text()).toBe(propsData.title);
+ });
+
+ it('renders the correct background color for the color item', () => {
+ const convertedColor = hexToRgb(propsData.color).join(', ');
+ expect(findColorItem().attributes('style')).toBe(`background-color: rgb(${convertedColor});`);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
new file mode 100644
index 00000000000..93b59800c27
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -0,0 +1,192 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
+import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
+import epicColorQuery from '~/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql';
+import updateEpicColorMutation from '~/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql';
+import ColorSelectRoot from '~/vue_shared/components/color_select_dropdown/color_select_root.vue';
+import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
+import { colorQueryResponse, updateColorMutationResponse, color } from './mock_data';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const successfulQueryHandler = jest.fn().mockResolvedValue(colorQueryResponse);
+const successfulMutationHandler = jest.fn().mockResolvedValue(updateColorMutationResponse);
+const errorQueryHandler = jest.fn().mockRejectedValue('Error fetching epic color.');
+const errorMutationHandler = jest.fn().mockRejectedValue('An error occurred while updating color.');
+
+const defaultProps = {
+ allowEdit: true,
+ iid: '1',
+ fullPath: 'workspace-1',
+};
+
+describe('LabelsSelectRoot', () => {
+ let wrapper;
+
+ const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findDropdownValue = () => wrapper.findComponent(DropdownValue);
+ const findDropdownContents = () => wrapper.findComponent(DropdownContents);
+
+ const createComponent = ({
+ queryHandler = successfulQueryHandler,
+ mutationHandler = successfulMutationHandler,
+ propsData,
+ } = {}) => {
+ const mockApollo = createMockApollo([
+ [epicColorQuery, queryHandler],
+ [updateEpicColorMutation, mutationHandler],
+ ]);
+
+ wrapper = shallowMount(ColorSelectRoot, {
+ apolloProvider: mockApollo,
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ provide: {
+ canUpdate: true,
+ },
+ stubs: {
+ SidebarEditableItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ const defaultClasses = ['labels-select-wrapper', 'gl-relative'];
+
+ it.each`
+ variant | cssClass
+ ${'sidebar'} | ${defaultClasses}
+ ${'embedded'} | ${[...defaultClasses, 'is-embedded']}
+ `(
+ 'renders component root element with CSS class `$cssClass` when variant is "$variant"',
+ async ({ variant, cssClass }) => {
+ createComponent({
+ propsData: { variant },
+ });
+
+ expect(wrapper.classes()).toEqual(cssClass);
+ },
+ );
+ });
+
+ describe('if the variant is `sidebar`', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders SidebarEditableItem component', () => {
+ expect(findSidebarEditableItem().exists()).toBe(true);
+ });
+
+ it('renders correct props for the SidebarEditableItem component', () => {
+ expect(findSidebarEditableItem().props()).toMatchObject({
+ title: wrapper.vm.$options.i18n.widgetTitle,
+ canEdit: defaultProps.allowEdit,
+ loading: true,
+ });
+ });
+
+ describe('when colors are loaded', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('passes false `loading` prop to sidebar editable item', () => {
+ expect(findSidebarEditableItem().props('loading')).toBe(false);
+ });
+
+ it('renders dropdown value component when query colors is resolved', () => {
+ expect(findDropdownValue().props('selectedColor')).toMatchObject(color);
+ });
+ });
+ });
+
+ describe('if the variant is `embedded`', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { iid: undefined, variant: DROPDOWN_VARIANT.Embedded } });
+ });
+
+ it('renders DropdownContents component', () => {
+ expect(findDropdownContents().exists()).toBe(true);
+ });
+
+ it('renders correct props for the DropdownContents component', () => {
+ expect(findDropdownContents().props()).toMatchObject({
+ variant: DROPDOWN_VARIANT.Embedded,
+ dropdownTitle: wrapper.vm.$options.i18n.assignColor,
+ dropdownButtonText: wrapper.vm.$options.i18n.dropdownButtonText,
+ });
+ });
+
+ it('handles DropdownContents setColor', () => {
+ findDropdownContents().vm.$emit('setColor', color);
+ expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
+ });
+ });
+
+ describe('when epicColorQuery errored', () => {
+ beforeEach(async () => {
+ createComponent({ queryHandler: errorQueryHandler });
+ await waitForPromises();
+ });
+
+ it('creates flash with error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ captureError: true,
+ message: 'Error fetching epic color.',
+ });
+ });
+ });
+
+ it('emits `updateSelectedColor` event on dropdown contents `setColor` event if iid is not set', () => {
+ createComponent({ propsData: { iid: undefined } });
+
+ findDropdownContents().vm.$emit('setColor', color);
+ expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
+ });
+
+ describe('when updating color for epic', () => {
+ beforeEach(() => {
+ createComponent();
+ findDropdownContents().vm.$emit('setColor', color);
+ });
+
+ it('sets the loading state', () => {
+ expect(findSidebarEditableItem().props('loading')).toBe(true);
+ });
+
+ it('updates color correctly after successful mutation', async () => {
+ await waitForPromises();
+ expect(findDropdownValue().props('selectedColor').color).toEqual(
+ updateColorMutationResponse.data.updateIssuableColor.issuable.color,
+ );
+ });
+
+ it('displays an error if mutation was rejected', async () => {
+ createComponent({ mutationHandler: errorMutationHandler });
+ findDropdownContents().vm.$emit('setColor', color);
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.anything(),
+ message: 'An error occurred while updating color.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
new file mode 100644
index 00000000000..303824c77b3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
@@ -0,0 +1,43 @@
+import { GlDropdownForm } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
+import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
+import { ISSUABLE_COLORS } from '~/vue_shared/components/color_select_dropdown/constants';
+import { color as defaultColor } from './mock_data';
+
+const propsData = {
+ selectedColor: defaultColor,
+};
+
+describe('DropdownContentsColorView', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(DropdownContentsColorView, {
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findColors = () => wrapper.findAllComponents(ColorItem);
+ const findColorList = () => wrapper.findComponent(GlDropdownForm);
+
+ it('renders color list', async () => {
+ expect(findColorList().exists()).toBe(true);
+ expect(findColors()).toHaveLength(ISSUABLE_COLORS.length);
+ });
+
+ it.each(ISSUABLE_COLORS)('emits an `input` event with %o on click on the option %#', (color) => {
+ const colorIndex = ISSUABLE_COLORS.indexOf(color);
+ findColors().at(colorIndex).trigger('click');
+
+ expect(wrapper.emitted('input')[0][0]).toMatchObject(color);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
new file mode 100644
index 00000000000..74f50b878e2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
@@ -0,0 +1,113 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
+import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
+import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
+
+import { color } from './mock_data';
+
+const showDropdown = jest.fn();
+const focusInput = jest.fn();
+
+const defaultProps = {
+ dropdownTitle: '',
+ selectedColor: color,
+ dropdownButtonText: '',
+ variant: '',
+ isVisible: false,
+};
+
+const GlDropdownStub = {
+ template: `
+ <div>
+ <slot name="header"></slot>
+ <slot></slot>
+ </div>
+ `,
+ methods: {
+ show: showDropdown,
+ hide: jest.fn(),
+ },
+};
+
+const DropdownHeaderStub = {
+ template: `
+ <div>Hello, I am a header</div>
+ `,
+ methods: {
+ focusInput,
+ },
+};
+
+describe('DropdownContent', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMount(DropdownContents, {
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ stubs: {
+ GlDropdown: GlDropdownStub,
+ DropdownHeader: DropdownHeaderStub,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findColorView = () => wrapper.findComponent(DropdownContentsColorView);
+ const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub);
+ const findDropdown = () => wrapper.findComponent(GlDropdownStub);
+
+ it('calls dropdown `show` method on `isVisible` prop change', async () => {
+ createComponent();
+ await wrapper.setProps({
+ isVisible: true,
+ });
+
+ expect(showDropdown).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not emit `setColor` event on dropdown hide if color did not change', () => {
+ createComponent();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setColor')).toBeUndefined();
+ });
+
+ it('emits `setColor` event on dropdown hide if color changed on non-sidebar widget', async () => {
+ createComponent({ propsData: { variant: DROPDOWN_VARIANT.Embedded } });
+ const updatedColor = {
+ title: 'Blue-gray',
+ color: '#6699cc',
+ };
+ findColorView().vm.$emit('input', updatedColor);
+ await nextTick();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]);
+ });
+
+ it('emits `setColor` event on visibility change if color changed on sidebar widget', async () => {
+ createComponent({ propsData: { variant: DROPDOWN_VARIANT.Sidebar, isVisible: true } });
+ const updatedColor = {
+ title: 'Blue-gray',
+ color: '#6699cc',
+ };
+ findColorView().vm.$emit('input', updatedColor);
+ wrapper.setProps({ isVisible: false });
+ await nextTick();
+
+ expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]);
+ });
+
+ it('renders header', () => {
+ createComponent();
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
new file mode 100644
index 00000000000..d203d78477f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import DropdownHeader from '~/vue_shared/components/color_select_dropdown/dropdown_header.vue';
+
+const propsData = {
+ dropdownTitle: 'Epic color',
+};
+
+describe('DropdownHeader', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(DropdownHeader, { propsData });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the correct title', () => {
+ expect(wrapper.text()).toBe(propsData.dropdownTitle);
+ });
+
+ it('renders a close button', () => {
+ expect(findButton().attributes('aria-label')).toBe('Close');
+ });
+
+ it('emits `closeDropdown` event on button click', () => {
+ expect(wrapper.emitted('closeDropdown')).toBeUndefined();
+ findButton().vm.$emit('click');
+
+ expect(wrapper.emitted('closeDropdown')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
new file mode 100644
index 00000000000..f22592dd604
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
@@ -0,0 +1,46 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
+import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
+
+import { color } from './mock_data';
+
+const propsData = {
+ selectedColor: color,
+};
+
+describe('DropdownValue', () => {
+ let wrapper;
+
+ const findColorItems = () => wrapper.findAllComponents(ColorItem);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(DropdownValue, { propsData });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when there is a color set', () => {
+ it('renders the color', () => {
+ expect(findColorItems()).toHaveLength(2);
+ });
+
+ it.each`
+ index | cssClass
+ ${0} | ${['gl-font-base', 'gl-line-height-24']}
+ ${1} | ${['hide-collapsed']}
+ `(
+ 'passes correct props to the ColorItem with CSS class `$cssClass`',
+ async ({ index, cssClass }) => {
+ expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor);
+ expect(findColorItems().at(index).classes()).toEqual(cssClass);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js
new file mode 100644
index 00000000000..097f47cc731
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js
@@ -0,0 +1,30 @@
+export const color = {
+ color: '#217645',
+ title: 'Green',
+};
+
+export const colorQueryResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Workspace/1',
+ issuable: {
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/1',
+ color: '#217645',
+ },
+ },
+ },
+};
+
+export const updateColorMutationResponse = {
+ data: {
+ updateIssuableColor: {
+ issuable: {
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/1',
+ color: '#217645',
+ },
+ errors: [],
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
index 9d11fbbaf55..e1860d3399b 100644
--- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -29,8 +29,8 @@ describe('ConfidentialityBadge', () => {
it.each`
workspaceType | issuableType | expectedTooltip
- ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least Reporter role can view or be notified about this issue.'}
- ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least Reporter role can view or be notified about this epic.'}
+ ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
+ ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
`(
'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
({ workspaceType, issuableType, expectedTooltip }) => {
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 1397fb0405e..01ef52c6af9 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -1,3 +1,4 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
@@ -39,10 +40,10 @@ describe('MarkdownViewer', () => {
});
});
- it('renders an animation container while the markdown is loading', () => {
+ it('renders a skeleton loader while the markdown is loading', () => {
createComponent();
- expect(wrapper.find('.animation-container').exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
it('renders markdown preview preview renders and loads rendered markdown from server', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index f03a2e7934f..51161a1a0ef 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -77,7 +77,7 @@ describe('LabelToken', () => {
describe('getActiveLabel', () => {
it('returns label object from labels array based on provided `currentValue` param', () => {
- expect(wrapper.vm.getActiveLabel(mockLabels, 'foo label')).toEqual(mockRegularLabel);
+ expect(wrapper.vm.getActiveLabel(mockLabels, 'Foo Label')).toEqual(mockRegularLabel);
});
});
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index e636f58d868..e1da8b690af 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -66,7 +66,7 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value as hidden', () => {
- expect(findFormInputGroup().props('value')).toBe('********************');
+ expect(findFormInput().element.value).toBe('********************');
});
it('saves actual value to clipboard when manually copied', () => {
@@ -77,6 +77,16 @@ describe('InputCopyToggleVisibility', () => {
expect(event.preventDefault).toHaveBeenCalled();
});
+ it('emits `copy` event when manually copied the token', () => {
+ expect(wrapper.emitted('copy')).toBeUndefined();
+
+ findFormInput().element.dispatchEvent(createCopyEvent());
+
+ expect(wrapper.emitted()).toHaveProperty('copy');
+ expect(wrapper.emitted('copy')).toHaveLength(1);
+ expect(wrapper.emitted('copy')[0]).toEqual([]);
+ });
+
describe('visibility toggle button', () => {
it('renders a reveal button', () => {
const revealButton = findRevealButton();
@@ -97,7 +107,7 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value', () => {
- expect(findFormInputGroup().props('value')).toBe(valueProp);
+ expect(findFormInput().element.value).toBe(valueProp);
});
it('renders a hide button', () => {
@@ -135,6 +145,8 @@ describe('InputCopyToggleVisibility', () => {
});
it('emits `copy` event', () => {
+ expect(wrapper.emitted()).toHaveProperty('copy');
+ expect(wrapper.emitted('copy')).toHaveLength(1);
expect(wrapper.emitted('copy')[0]).toEqual([]);
});
});
@@ -147,25 +159,52 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value as hidden with 20 asterisks', () => {
- expect(findFormInputGroup().props('value')).toBe('********************');
+ expect(findFormInput().element.value).toBe('********************');
});
});
describe('when `initialVisibility` prop is `true`', () => {
+ const label = 'My label';
+
beforeEach(() => {
createComponent({
propsData: {
value: valueProp,
initialVisibility: true,
+ label,
+ 'label-for': 'my-input',
+ formInputGroupProps: {
+ id: 'my-input',
+ },
},
});
});
it('displays value', () => {
- expect(findFormInputGroup().props('value')).toBe(valueProp);
+ expect(findFormInput().element.value).toBe(valueProp);
});
itDoesNotModifyCopyEvent();
+
+ describe('when input is clicked', () => {
+ it('selects input value', async () => {
+ const mockSelect = jest.fn();
+ wrapper.vm.$refs.input.$el.select = mockSelect;
+ await wrapper.findByLabelText(label).trigger('click');
+
+ expect(mockSelect).toHaveBeenCalled();
+ });
+ });
+
+ describe('when label is clicked', () => {
+ it('selects input value', async () => {
+ const mockSelect = jest.fn();
+ wrapper.vm.$refs.input.$el.select = mockSelect;
+ await wrapper.find('label').trigger('click');
+
+ expect(mockSelect).toHaveBeenCalled();
+ });
+ });
});
describe('when `showToggleVisibilityButton` is `false`', () => {
@@ -184,7 +223,7 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value', () => {
- expect(findFormInputGroup().props('value')).toBe(valueProp);
+ expect(findFormInput().element.value).toBe(valueProp);
});
itDoesNotModifyCopyEvent();
@@ -204,16 +243,30 @@ describe('InputCopyToggleVisibility', () => {
});
});
- it('passes `formInputGroupProps` prop to `GlFormInputGroup`', () => {
+ it('passes `formInputGroupProps` prop only to the input', () => {
createComponent({
propsData: {
formInputGroupProps: {
- label: 'Foo bar',
+ name: 'Foo bar',
+ 'data-qa-selector': 'Foo bar',
+ class: 'Foo bar',
+ id: 'Foo bar',
},
},
});
- expect(findFormInputGroup().props('label')).toBe('Foo bar');
+ expect(findFormInput().attributes()).toMatchObject({
+ name: 'Foo bar',
+ 'data-qa-selector': 'Foo bar',
+ class: expect.stringContaining('Foo bar'),
+ id: 'Foo bar',
+ });
+
+ const attributesInputGroup = findFormInputGroup().attributes();
+ expect(attributesInputGroup.name).toBeUndefined();
+ expect(attributesInputGroup['data-qa-selector']).toBeUndefined();
+ expect(attributesInputGroup.class).not.toContain('Foo bar');
+ expect(attributesInputGroup.id).toBeUndefined();
});
it('passes `copyButtonTitle` prop to `ClipboardButton`', () => {
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
index f878d685b6d..8a187f3cb1f 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
@@ -10,7 +10,7 @@ exports[`Issue Warning Component when issue is locked but not confidential rende
href="locked-path"
target="_blank"
>
- Learn more
+ Learn more.
</gl-link-stub>
</span>
`;
@@ -25,7 +25,7 @@ exports[`Issue Warning Component when noteable is confidential but not locked re
href="confidential-path"
target="_blank"
>
- Learn more
+ Learn more.
</gl-link-stub>
</span>
`;
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 65f79bab005..98b04ede943 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,13 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
+import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
-import initMRPopovers from '~/mr_popover/index';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import axios from '~/lib/utils/axios_utils';
-jest.mock('~/mr_popover/index', () => jest.fn());
-
describe('system note component', () => {
let vm;
let props;
@@ -76,10 +74,12 @@ describe('system note component', () => {
expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>');
});
- it('should initMRPopovers onMount', () => {
+ it('should renderGFM onMount', () => {
+ const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
+
createComponent(props);
- expect(initMRPopovers).toHaveBeenCalled();
+ expect(renderGFMSpy).toHaveBeenCalled();
});
it('renders outdated code lines', async () => {
diff --git a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
index 9be2de17d01..ff4febd647e 100644
--- a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
+++ b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
@@ -22,7 +22,7 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', ()
it('should render alert with correct props', async () => {
createComponent({ errorMessages: [{ code: 'MissingQuotes' }] });
- await nextTick;
+ await nextTick();
expect(findAlert().props()).toMatchObject({
variant: 'danger',
@@ -37,7 +37,7 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', ()
createComponent({
errorMessages: [{ code: 'NotDefined', message: 'Error code is undefined' }],
});
- await nextTick;
+ await nextTick();
expect(findAlert().text()).toContain('Error code is undefined');
});
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
index f5ef5b3d443..20716e79a04 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -11,7 +11,7 @@ describe('Registry Search', () => {
const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const defaultProps = {
- filter: [],
+ filters: [],
sorting: { sort: 'asc', orderBy: 'name' },
tokens: [{ type: 'foo' }],
sortableFields: [
@@ -123,7 +123,7 @@ describe('Registry Search', () => {
});
describe('query string calculation', () => {
- const filter = [
+ const filters = [
{ type: FILTERED_SEARCH_TERM, value: { data: 'one' } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'two' } },
{ type: 'typeOne', value: { data: 'value_one' } },
@@ -131,7 +131,7 @@ describe('Registry Search', () => {
];
it('aggregates the filter in the correct object', () => {
- mountComponent({ ...defaultProps, filter });
+ mountComponent({ ...defaultProps, filters });
findFilteredSearch().vm.$emit('submit');
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 001b6ee4a6f..7173abe1316 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -48,12 +48,12 @@ describe('RunnerInstructionsModal component', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
- const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
+ const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button');
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
const findRegisterCommand = () => wrapper.findByTestId('register-command');
- const createComponent = ({ props, ...options } = {}) => {
+ const createComponent = ({ props, shown = true, ...options } = {}) => {
const requestHandlers = [
[getRunnerPlatformsQuery, runnerPlatformsHandler],
[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
@@ -72,169 +72,202 @@ describe('RunnerInstructionsModal component', () => {
...options,
}),
);
+
+ // trigger open modal
+ if (shown) {
+ findModal().vm.$emit('shown');
+ }
};
beforeEach(async () => {
runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
-
- createComponent();
- await waitForPromises();
});
afterEach(() => {
wrapper.destroy();
});
- it('should not show alert', () => {
- expect(findAlert().exists()).toBe(false);
- });
-
- it('should contain a number of platforms buttons', () => {
- expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
+ describe('when the modal is shown', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
- const buttons = findPlatformButtons();
+ it('should not show alert', async () => {
+ expect(findAlert().exists()).toBe(false);
+ });
- expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
- });
+ it('should contain a number of platforms buttons', () => {
+ expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
- it('should contain a number of dropdown items for the architecture options', () => {
- expect(findArchitectureDropdownItems()).toHaveLength(
- mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
- );
- });
+ const buttons = findPlatformButtons();
- describe('should display default instructions', () => {
- const { installInstructions, registerInstructions } = mockGraphqlInstructions.data.runnerSetup;
+ expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
+ });
- it('runner instructions are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
- platform: 'linux',
- architecture: 'amd64',
- });
+ it('should contain a number of dropdown items for the architecture options', () => {
+ expect(findArchitectureDropdownItems()).toHaveLength(
+ mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
+ );
});
- it('binary instructions are shown', async () => {
- await waitForPromises();
- const instructions = findBinaryInstructions().text();
+ describe('should display default instructions', () => {
+ const {
+ installInstructions,
+ registerInstructions,
+ } = mockGraphqlInstructions.data.runnerSetup;
- expect(instructions).toBe(installInstructions);
- });
+ it('runner instructions are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
+ platform: 'linux',
+ architecture: 'amd64',
+ });
+ });
- it('register command is shown with a replaced token', async () => {
- await waitForPromises();
- const instructions = findRegisterCommand().text();
+ it('binary instructions are shown', async () => {
+ const instructions = findBinaryInstructions().text();
- expect(instructions).toBe(
- 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
- );
- });
+ expect(instructions).toBe(installInstructions);
+ });
- describe('when a register token is not shown', () => {
- beforeEach(async () => {
- createComponent({ props: { registrationToken: undefined } });
- await waitForPromises();
+ it('register command is shown with a replaced token', async () => {
+ const command = findRegisterCommand().text();
+
+ expect(command).toBe(
+ 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
});
- it('register command is shown without a defined registration token', () => {
- const instructions = findRegisterCommand().text();
+ describe('when a register token is not shown', () => {
+ beforeEach(async () => {
+ createComponent({ props: { registrationToken: undefined } });
+ await waitForPromises();
+ });
+
+ it('register command is shown without a defined registration token', () => {
+ const instructions = findRegisterCommand().text();
- expect(instructions).toBe(registerInstructions);
+ expect(instructions).toBe(registerInstructions);
+ });
});
- });
- describe('when the modal is shown', () => {
- it('sets the focus on the selected platform', () => {
- findPlatformButtons().at(0).element.focus = jest.fn();
+ describe('when providing a defaultPlatformName', () => {
+ beforeEach(async () => {
+ createComponent({ props: { defaultPlatformName: 'osx' } });
+ await waitForPromises();
+ });
+
+ it('runner instructions for the default selected platform are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
+ platform: 'osx',
+ architecture: 'amd64',
+ });
+ });
+
+ it('sets the focus on the default selected platform', () => {
+ const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
+
+ findOsxPlatformButton().element.focus = jest.fn();
- findModal().vm.$emit('shown');
+ findModal().vm.$emit('shown');
- expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled();
+ expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
+ });
});
});
- describe('when providing a defaultPlatformName', () => {
+ describe('after a platform and architecture are selected', () => {
+ const windowsIndex = 2;
+ const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
+
beforeEach(async () => {
- createComponent({ props: { defaultPlatformName: 'osx' } });
+ runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
+
+ findPlatformButtons().at(windowsIndex).vm.$emit('click');
await waitForPromises();
});
- it('runner instructions for the default selected platform are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
- platform: 'osx',
+ it('runner instructions are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
+ platform: 'windows',
architecture: 'amd64',
});
});
- it('sets the focus on the default selected platform', () => {
- findOsxPlatformButton().element.focus = jest.fn();
-
- findModal().vm.$emit('shown');
+ it('architecture download link is updated', () => {
+ const architectures =
+ mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes;
- expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
+ expect(findBinaryDownloadButton().attributes('href')).toBe(
+ architectures[0].downloadLocation,
+ );
});
- });
- });
- describe('after a platform and architecture are selected', () => {
- const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
+ it('other binary instructions are shown', () => {
+ const instructions = findBinaryInstructions().text();
- beforeEach(async () => {
- runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
+ expect(instructions).toBe(installInstructions);
+ });
- findPlatformButtons().at(2).vm.$emit('click'); // another option, happens to be windows
- await nextTick();
+ it('register command is shown', () => {
+ const command = findRegisterCommand().text();
- findArchitectureDropdownItems().at(1).vm.$emit('click'); // another option
- await nextTick();
- });
+ expect(command).toBe(
+ './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
+ });
+
+ it('runner instructions are requested with another architecture', async () => {
+ findArchitectureDropdownItems().at(1).vm.$emit('click');
+ await waitForPromises();
- it('runner instructions are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
- platform: 'windows',
- architecture: '386',
+ expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
+ platform: 'windows',
+ architecture: '386',
+ });
});
});
- it('other binary instructions are shown', () => {
- const instructions = findBinaryInstructions().text();
+ describe('when the modal resizes', () => {
+ it('to an xs viewport', async () => {
+ MockResizeObserver.mockResize('xs');
+ await nextTick();
- expect(instructions).toBe(installInstructions);
- });
+ expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
+ });
- it('register command is shown', () => {
- const command = findRegisterCommand().text();
+ it('to a non-xs viewport', async () => {
+ MockResizeObserver.mockResize('sm');
+ await nextTick();
- expect(command).toBe(
- './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
- );
+ expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
+ });
});
});
- describe('when the modal resizes', () => {
- it('to an xs viewport', async () => {
- MockResizeObserver.mockResize('xs');
- await nextTick();
-
- expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
+ describe('when the modal is not shown', () => {
+ beforeEach(async () => {
+ createComponent({ shown: false });
+ await waitForPromises();
});
- it('to a non-xs viewport', async () => {
- MockResizeObserver.mockResize('sm');
- await nextTick();
-
- expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
+ it('does not fetch instructions', () => {
+ expect(runnerPlatformsHandler).not.toHaveBeenCalled();
+ expect(runnerSetupInstructionsHandler).not.toHaveBeenCalled();
});
});
describe('when apollo is loading', () => {
- it('should show a skeleton loader', async () => {
+ beforeEach(() => {
createComponent();
+ });
+
+ it('should show a skeleton loader', async () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findGlLoadingIcon().exists()).toBe(false);
- await nextTick();
- jest.runOnlyPendingTimers();
+ // wait on fetch of both `platforms` and `instructions`
await nextTick();
await nextTick();
@@ -242,7 +275,6 @@ describe('RunnerInstructionsModal component', () => {
});
it('once loaded, should not show a loading state', async () => {
- createComponent();
await waitForPromises();
expect(findSkeletonLoader().exists()).toBe(false);
@@ -255,7 +287,6 @@ describe('RunnerInstructionsModal component', () => {
runnerSetupInstructionsHandler.mockRejectedValue();
createComponent();
-
await waitForPromises();
});
@@ -287,6 +318,7 @@ describe('RunnerInstructionsModal component', () => {
mockShow = jest.fn();
createComponent({
+ shown: false,
stubs: {
GlModal: getGlModalStub({ show: mockShow }),
},
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
index 9a95a838291..986d76d2b95 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -1,6 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
@@ -11,7 +10,11 @@ describe('RunnerInstructions component', () => {
const findModal = () => wrapper.findComponent(RunnerInstructionsModal);
const createComponent = () => {
- wrapper = extendedWrapper(shallowMount(RunnerInstructions));
+ wrapper = shallowMountExtended(RunnerInstructions, {
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ });
};
beforeEach(() => {
@@ -23,19 +26,12 @@ describe('RunnerInstructions component', () => {
});
it('should show the "Show runner installation instructions" button', () => {
- expect(findModalButton().exists()).toBe(true);
expect(findModalButton().text()).toBe('Show runner installation instructions');
});
- it('should not render the modal once mounted', () => {
- expect(findModal().exists()).toBe(false);
- });
-
- it('should render the modal once clicked', async () => {
- findModalButton().vm.$emit('click');
-
- await nextTick();
+ it('should render the modal', () => {
+ const modalId = getBinding(findModal().element, 'gl-modal');
- expect(findModal().exists()).toBe(true);
+ expect(findModalButton().attributes('modal-id')).toBe(modalId);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 42202db4935..00c8e3a814a 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -226,12 +226,7 @@ describe('DropdownContentsLabelsView', () => {
preventDefault: fakePreventDefault,
});
- expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([
- {
- ...mockLabels[2],
- set: true,
- },
- ]);
+ expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockLabels[2]]);
});
it('calls action `toggleDropdownContents` when Esc key is pressed', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
index bd1705e7693..bedb6204088 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
@@ -45,18 +45,26 @@ describe('LabelItem', () => {
wrapperTemp.destroy();
});
- it('renders visible gl-icon component when `isLabelSet` prop is true', () => {
- const wrapperTemp = createComponent({
- isLabelSet: true,
- });
-
- const iconEl = wrapperTemp.find(GlIcon);
-
- expect(iconEl.isVisible()).toBe(true);
- expect(iconEl.props('name')).toBe('mobile-issue-close');
-
- wrapperTemp.destroy();
- });
+ it.each`
+ isLabelSet | isLabelIndeterminate | testId | iconName
+ ${true} | ${false} | ${'checked-icon'} | ${'mobile-issue-close'}
+ ${false} | ${true} | ${'indeterminate-icon'} | ${'dash'}
+ `(
+ 'renders visible gl-icon component when `isLabelSet` prop is $isLabelSet and `isLabelIndeterminate` is $isLabelIndeterminate',
+ ({ isLabelSet, isLabelIndeterminate, testId, iconName }) => {
+ const wrapperTemp = createComponent({
+ isLabelSet,
+ isLabelIndeterminate,
+ });
+
+ const iconEl = wrapperTemp.find(`[data-testid="${testId}"]`);
+
+ expect(iconEl.isVisible()).toBe(true);
+ expect(iconEl.props('name')).toBe(iconName);
+
+ wrapperTemp.destroy();
+ },
+ );
it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => {
const wrapperTemp = createComponent({
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 31819d0e2f7..c150410ff8e 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -46,9 +46,15 @@ describe('LabelsSelectRoot', () => {
describe('methods', () => {
describe('handleVuexActionDispatch', () => {
+ const touchedLabels = [
+ {
+ id: 2,
+ touched: true,
+ },
+ ];
+
it('calls `handleDropdownClose` when params `action.type` is `toggleDropdownContents` and state has `showDropdownButton` & `showDropdownContents` props `false`', () => {
createComponent();
- jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
wrapper.vm.handleVuexActionDispatch(
{ type: 'toggleDropdownContents' },
@@ -59,14 +65,12 @@ describe('LabelsSelectRoot', () => {
},
);
- expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
- expect.arrayContaining([
- {
- id: 2,
- touched: true,
- },
- ]),
- );
+ // We're utilizing `onDropdownClose` event emitted from the component to always include `touchedLabels`
+ // while the first param of the method is the labels list which were added/removed.
+ expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy();
+ expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([touchedLabels]);
+ expect(wrapper.emitted('onDropdownClose')).toBeTruthy();
+ expect(wrapper.emitted('onDropdownClose')[0]).toEqual([touchedLabels]);
});
it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
@@ -75,8 +79,6 @@ describe('LabelsSelectRoot', () => {
variant: 'embedded',
});
- jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
-
wrapper.vm.handleVuexActionDispatch(
{ type: 'toggleDropdownContents' },
{
@@ -86,34 +88,17 @@ describe('LabelsSelectRoot', () => {
},
);
- expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
- expect.arrayContaining([
+ expect(wrapper.emitted('updateSelectedLabels')).toBeTruthy();
+ expect(wrapper.emitted('updateSelectedLabels')[0]).toEqual([
+ [
{
id: 2,
set: true,
},
- ]),
- );
- });
- });
-
- describe('handleDropdownClose', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('emits `updateSelectedLabels` & `onDropdownClose` events on component when provided `labels` param is not empty', () => {
- wrapper.vm.handleDropdownClose([{ id: 1 }, { id: 2 }]);
-
- expect(wrapper.emitted().updateSelectedLabels).toBeTruthy();
- expect(wrapper.emitted().onDropdownClose).toBeTruthy();
- });
-
- it('emits only `onDropdownClose` event on component when provided `labels` param is empty', () => {
- wrapper.vm.handleDropdownClose([]);
-
- expect(wrapper.emitted().updateSelectedLabels).toBeFalsy();
- expect(wrapper.emitted().onDropdownClose).toBeTruthy();
+ ],
+ ]);
+ expect(wrapper.emitted('onDropdownClose')).toBeTruthy();
+ expect(wrapper.emitted('onDropdownClose')[0]).toEqual([[]]);
});
});
@@ -152,13 +137,13 @@ describe('LabelsSelectRoot', () => {
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
createComponent();
- await nextTick;
+ await nextTick();
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
it('renders `dropdown-title` component', async () => {
createComponent();
- await nextTick;
+ await nextTick();
expect(wrapper.find(DropdownTitle).exists()).toBe(true);
});
@@ -166,7 +151,7 @@ describe('LabelsSelectRoot', () => {
createComponent(mockConfig, {
default: 'None',
});
- await nextTick;
+ await nextTick();
const valueComp = wrapper.find(DropdownValue);
@@ -177,14 +162,14 @@ describe('LabelsSelectRoot', () => {
it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownButton');
- await nextTick;
+ await nextTick();
expect(wrapper.find(DropdownButton).exists()).toBe(true);
});
it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => {
createComponent();
wrapper.vm.$store.dispatch('toggleDropdownContents');
- await nextTick;
+ await nextTick();
expect(wrapper.find(DropdownContents).exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
index 1f899e84897..6ad46dbe898 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
@@ -17,24 +17,39 @@ describe('LabelsSelect Getters', () => {
},
);
- it('returns label title when state.labels has only 1 label', () => {
- const labels = [{ id: 1, title: 'Foobar', set: true }];
+ describe.each`
+ dropdownVariant | isDropdownVariantSidebar | isDropdownVariantEmbedded
+ ${'sidebar'} | ${true} | ${false}
+ ${'embedded'} | ${false} | ${true}
+ `(
+ 'when dropdown variant is $dropdownVariant',
+ ({ isDropdownVariantSidebar, isDropdownVariantEmbedded }) => {
+ it('returns label title when state.labels has only 1 label', () => {
+ const labels = [{ id: 1, title: 'Foobar', set: true }];
- expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
- 'Foobar',
- );
- });
+ expect(
+ getters.dropdownButtonText(
+ { labels },
+ { isDropdownVariantSidebar, isDropdownVariantEmbedded },
+ ),
+ ).toBe('Foobar');
+ });
- it('returns first label title and remaining labels count when state.labels has more than 1 label', () => {
- const labels = [
- { id: 1, title: 'Foo', set: true },
- { id: 2, title: 'Bar', set: true },
- ];
+ it('returns first label title and remaining labels count when state.labels has more than 1 label', () => {
+ const labels = [
+ { id: 1, title: 'Foo', set: true },
+ { id: 2, title: 'Bar', set: true },
+ ];
- expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
- 'Foo +1 more',
- );
- });
+ expect(
+ getters.dropdownButtonText(
+ { labels },
+ { isDropdownVariantSidebar, isDropdownVariantEmbedded },
+ ),
+ ).toBe('Foo +1 more');
+ });
+ },
+ );
});
describe('selectedLabelsList', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index a60e6f52862..1819e750324 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -80,7 +80,10 @@ describe('LabelsSelect Mutations', () => {
});
describe(`${types.RECEIVE_SET_LABELS_SUCCESS}`, () => {
- const selectedLabels = [{ id: 2 }, { id: 4 }];
+ const selectedLabels = [
+ { id: 2, set: true },
+ { id: 4, set: true },
+ ];
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
it('sets value of `state.labelsFetchInProgress` to false', () => {
@@ -196,20 +199,23 @@ describe('LabelsSelect Mutations', () => {
it('updates labels `set` state to match selected labels', () => {
const state = {
labels: [
- { id: 1, title: 'scoped::test', set: false },
- { id: 2, set: true, title: 'scoped::one', touched: true },
- { id: 3, title: '' },
- { id: 4, title: '' },
+ { id: 1, title: 'scoped::test', set: false, indeterminate: false },
+ { id: 2, title: 'scoped::one', set: true, indeterminate: false, touched: true },
+ { id: 3, title: '', set: false, indeterminate: false },
+ { id: 4, title: '', set: false, indeterminate: false },
+ ],
+ selectedLabels: [
+ { id: 1, set: true },
+ { id: 3, set: true },
],
- selectedLabels: [{ id: 1 }, { id: 3 }],
};
mutations[types.UPDATE_LABELS_SET_STATE](state);
expect(state.labels).toEqual([
- { id: 1, title: 'scoped::test', set: true },
- { id: 2, set: false, title: 'scoped::one', touched: true },
- { id: 3, title: '', set: true },
- { id: 4, title: '', set: false },
+ { id: 1, title: 'scoped::test', set: true, indeterminate: false },
+ { id: 2, title: 'scoped::one', set: false, indeterminate: false, touched: true },
+ { id: 3, title: '', set: true, indeterminate: false },
+ { id: 4, title: '', set: false, indeterminate: false },
]);
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
new file mode 100644
index 00000000000..83fdc5d669d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
@@ -0,0 +1,14 @@
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants';
+import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
+
+jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments');
+const hljsMock = { addPlugin: jest.fn() };
+
+describe('Highlight.js plugin registration', () => {
+ beforeEach(() => registerPlugins(hljsMock));
+
+ it('registers our plugins', () => {
+ expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
new file mode 100644
index 00000000000..5fd4182da29
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
@@ -0,0 +1,29 @@
+import { HLJS_COMMENT_SELECTOR } from '~/vue_shared/components/source_viewer/constants';
+import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
+
+describe('Highlight.js plugin for wrapping comments', () => {
+ it('mutates the input value by wrapping each line in a span tag', () => {
+ const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n* Line 2 \n*/</span>`;
+ const outputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n<span class="${HLJS_COMMENT_SELECTOR}">* Line 2 </span>\n<span class="${HLJS_COMMENT_SELECTOR}">*/</span>`;
+ const hljsResultMock = { value: inputValue };
+
+ wrapComments(hljsResultMock);
+ expect(hljsResultMock.value).toBe(outputValue);
+ });
+
+ it('does not mutate the input value if the hljs comment selector is not present', () => {
+ const inputValue = '<span class="hljs-keyword">const</span>';
+ const hljsResultMock = { value: inputValue };
+
+ wrapComments(hljsResultMock);
+ expect(hljsResultMock.value).toBe(inputValue);
+ });
+
+ it('does not mutate the input value if the hljs comment line includes a closing tag', () => {
+ const inputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 </span> \n* Line 2 \n*/`;
+ const hljsResultMock = { value: inputValue };
+
+ wrapComments(hljsResultMock);
+ expect(hljsResultMock.value).toBe(inputValue);
+ });
+});
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 6a9ea75127d..bb0945a1f3e 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
@@ -3,6 +3,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
@@ -11,6 +12,7 @@ import eventHub from '~/notes/event_hub';
jest.mock('~/blob/line_highlighter');
jest.mock('highlight.js/lib/core');
+jest.mock('~/vue_shared/components/source_viewer/plugins/index');
Vue.use(VueRouter);
const router = new VueRouter();
@@ -59,6 +61,10 @@ describe('Source Viewer component', () => {
describe('highlight.js', () => {
beforeEach(() => createComponent({ language: mappedLanguage }));
+ it('registers our plugins for Highlight.js', () => {
+ expect(registerPlugins).toHaveBeenCalledWith(hljs);
+ });
+
it('registers the language definition', async () => {
const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
index a613b325462..1798ca5ccde 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -5,7 +5,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
>
<div
@@ -41,7 +41,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style="display: none;"
>
<div
@@ -86,7 +86,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
>
<div
@@ -126,7 +126,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style=""
>
<div
@@ -171,7 +171,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
>
<div
@@ -211,7 +211,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style=""
>
<div
@@ -256,7 +256,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
>
<div
@@ -296,7 +296,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style=""
>
<div
@@ -342,7 +342,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
>
<div
@@ -382,7 +382,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style=""
>
<div
@@ -428,7 +428,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
>
<div
@@ -468,7 +468,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style="display: none;"
>
<div
@@ -514,7 +514,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4"
type="button"
>
<div
@@ -554,7 +554,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style="display: none;"
>
<div
@@ -606,7 +606,7 @@ exports[`Upload dropzone component when slot provided renders dropzone with slot
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
+ class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
style="display: none;"
>
<div
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 65eb42ef053..70017903079 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
@@ -5,9 +5,10 @@ import { shallowMountExtended as shallowMount } from 'helpers/vue_test_utils_hel
import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
-import { mockIssuable, mockRegularLabel, mockScopedLabel } from '../mock_data';
+import { mockIssuable, mockRegularLabel } from '../mock_data';
const createComponent = ({
+ hasScopedLabelsFeature = false,
issuableSymbol = '#',
issuable = mockIssuable,
showCheckbox = true,
@@ -15,6 +16,7 @@ const createComponent = ({
} = {}) =>
shallowMount(IssuableItem, {
propsData: {
+ hasScopedLabelsFeature,
issuableSymbol,
issuable,
showDiscussions: true,
@@ -182,21 +184,6 @@ describe('IssuableItem', () => {
});
describe('methods', () => {
- describe('scopedLabel', () => {
- it.each`
- label | labelType | returnValue
- ${mockRegularLabel} | ${'regular'} | ${false}
- ${mockScopedLabel} | ${'scoped'} | ${true}
- `(
- 'return $returnValue when provided label param is a $labelType label',
- ({ label, returnValue }) => {
- wrapper = createComponent();
-
- expect(wrapper.vm.scopedLabel(label)).toBe(returnValue);
- },
- );
- });
-
describe('labelTitle', () => {
it.each`
label | propWithTitle | returnValue
@@ -500,5 +487,21 @@ describe('IssuableItem', () => {
expect(wrapper.classes()).not.toContain('today');
});
});
+
+ describe('scoped labels', () => {
+ describe.each`
+ description | labelPosition | hasScopedLabelsFeature | scoped
+ ${'when label is not scoped and there is no scoped_labels feature'} | ${0} | ${false} | ${false}
+ ${'when label is scoped and there is no scoped_labels feature'} | ${1} | ${false} | ${false}
+ ${'when label is not scoped and there is scoped_labels feature'} | ${0} | ${true} | ${false}
+ ${'when label is scoped and there is scoped_labels feature'} | ${1} | ${true} | ${true}
+ `('$description', ({ hasScopedLabelsFeature, labelPosition, scoped }) => {
+ it(`${scoped ? 'renders' : 'does not render'} as scoped label`, () => {
+ wrapper = createComponent({ hasScopedLabelsFeature });
+
+ expect(wrapper.findAllComponents(GlLabel).at(labelPosition).props('scoped')).toBe(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 058cb30c1d5..66f71c0b028 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
@@ -1,9 +1,4 @@
-import {
- GlAlert,
- GlKeysetPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlPagination,
-} from '@gitlab/ui';
+import { GlAlert, GlKeysetPagination, GlSkeletonLoader, GlPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import VueDraggable from 'vuedraggable';
@@ -263,7 +258,7 @@ describe('IssuableListRoot', () => {
it('renders gl-loading-icon when `issuablesLoading` prop is true', () => {
wrapper = createComponent({ props: { issuablesLoading: true } });
- expect(wrapper.findAllComponents(GlSkeletonLoading)).toHaveLength(
+ expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(
wrapper.vm.skeletonItemCount,
);
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 1a93838b03f..7c582360637 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -159,7 +159,6 @@ describe('IssuableBody', () => {
expect(titleEl.exists()).toBe(true);
expect(titleEl.props()).toMatchObject({
issuable: issuableBodyProps.issuable,
- statusBadgeClass: issuableBodyProps.statusBadgeClass,
statusIcon: issuableBodyProps.statusIcon,
enableEdit: issuableBodyProps.enableEdit,
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 544db891a13..e00bb184535 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
+import { GlBadge, GlIcon, GlAvatarLabeled } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
@@ -69,7 +69,7 @@ describe('IssuableHeader', () => {
describe('template', () => {
it('renders issuable status icon and text', () => {
createComponent();
- const statusBoxEl = wrapper.findByTestId('status');
+ const statusBoxEl = wrapper.findComponent(GlBadge);
const statusIconEl = statusBoxEl.findComponent(GlIcon);
expect(statusBoxEl.exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index 8b027f990a2..f56064ed8e1 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -47,7 +47,6 @@ describe('IssuableShowRoot', () => {
describe('template', () => {
const {
- statusBadgeClass,
statusIcon,
statusIconClass,
enableEdit,
@@ -69,7 +68,6 @@ describe('IssuableShowRoot', () => {
expect(issuableHeader.exists()).toBe(true);
expect(issuableHeader.props()).toMatchObject({
issuableState: state,
- statusBadgeClass,
statusIcon,
statusIconClass,
blocked,
@@ -91,7 +89,6 @@ describe('IssuableShowRoot', () => {
expect(issuableBody.exists()).toBe(true);
expect(issuableBody.props()).toMatchObject({
issuable: mockIssuable,
- statusBadgeClass,
statusIcon,
enableEdit,
enableAutocomplete,
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index 11e3302d409..5aa67667033 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
+import { GlIcon, GlBadge, GlButton, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -40,7 +40,7 @@ describe('IssuableTitle', () => {
describe('methods', () => {
describe('handleTitleAppear', () => {
it('sets value of `stickyTitleVisible` prop to false', () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
expect(wrapper.vm.stickyTitleVisible).toBe(false);
});
@@ -48,7 +48,7 @@ describe('IssuableTitle', () => {
describe('handleTitleDisappear', () => {
it('sets value of `stickyTitleVisible` prop to true', () => {
- wrapper.find(GlIntersectionObserver).vm.$emit('disappear');
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
expect(wrapper.vm.stickyTitleVisible).toBe(true);
});
@@ -70,14 +70,14 @@ describe('IssuableTitle', () => {
expect(titleEl.exists()).toBe(true);
expect(titleEl.html()).toBe(
- '<h1 dir="auto" data-testid="title" class="title qa-title"><b>Sample</b> title</h1>',
+ '<h1 dir="auto" data-testid="title" class="title qa-title gl-font-size-h-display"><b>Sample</b> title</h1>',
);
wrapperWithTitle.destroy();
});
it('renders edit button', () => {
- const editButtonEl = wrapper.find(GlButton);
+ const editButtonEl = wrapper.findComponent(GlButton);
const tooltip = getBinding(editButtonEl.element, 'gl-tooltip');
expect(editButtonEl.exists()).toBe(true);
@@ -97,7 +97,10 @@ describe('IssuableTitle', () => {
const stickyHeaderEl = wrapper.find('[data-testid="header"]');
expect(stickyHeaderEl.exists()).toBe(true);
- expect(stickyHeaderEl.find(GlIcon).props('name')).toBe(issuableTitleProps.statusIcon);
+ expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
+ expect(stickyHeaderEl.findComponent(GlIcon).props('name')).toBe(
+ issuableTitleProps.statusIcon,
+ );
expect(stickyHeaderEl.text()).toContain('Open');
expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
});
diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js
index 32bb9edfe08..5ec205a2d5c 100644
--- a/spec/frontend/vue_shared/issuable/show/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/show/mock_data.js
@@ -36,7 +36,6 @@ export const mockIssuableShowProps = {
enableTaskList: true,
enableEdit: true,
showFieldTitle: false,
- statusBadgeClass: 'issuable-status-badge-open',
statusIcon: 'issues',
statusIconClass: 'gl-sm-display-none',
taskCompletionStatus: {
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index 0d85df25b4f..2c3f6ef8634 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -15,7 +15,7 @@ const createComponent = ({ title = 'Sample title', disabled = false } = {}) =>
describe('ItemTitle', () => {
let wrapper;
const mockUpdatedTitle = 'Updated title';
- const findInputEl = () => wrapper.find('span#item-title');
+ const findInputEl = () => wrapper.find('[aria-label="Title"]');
beforeEach(() => {
wrapper = createComponent();
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
new file mode 100644
index 00000000000..0552fe5050e
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -0,0 +1,93 @@
+import { GlLink, GlTokenSelector } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
+import localUpdateWorkItemMutation from '~/work_items/graphql/local_update_work_item.mutation.graphql';
+
+const mockAssignees = [
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ avatarUrl: '',
+ webUrl: '',
+ name: 'John Doe',
+ username: 'doe_I',
+ },
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/2',
+ avatarUrl: '',
+ webUrl: '',
+ name: 'Marcus Rutherford',
+ username: 'ruthfull',
+ },
+];
+
+const workItemId = 'gid://gitlab/WorkItem/1';
+
+const mutate = jest.fn();
+
+describe('WorkItemAssignees component', () => {
+ let wrapper;
+
+ const findAssigneeLinks = () => wrapper.findAllComponents(GlLink);
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+
+ const findEmptyState = () => wrapper.findByTestId('empty-state');
+
+ const createComponent = ({ assignees = mockAssignees } = {}) => {
+ wrapper = mountExtended(WorkItemAssignees, {
+ propsData: {
+ assignees,
+ workItemId,
+ },
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ attachTo: document.body,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should pass the correct data-user-id attribute', () => {
+ createComponent();
+
+ expect(findAssigneeLinks().at(0).attributes('data-user-id')).toBe('1');
+ });
+
+ describe('when there are assignees', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should focus token selector on token removal', async () => {
+ findTokenSelector().vm.$emit('token-remove', mockAssignees[0].id);
+ await nextTick();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
+ });
+
+ it('should call a mutation on clicking outside the token selector', async () => {
+ findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
+ findTokenSelector().vm.$emit('token-remove');
+ await nextTick();
+ expect(mutate).not.toHaveBeenCalled();
+
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+ await nextTick();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: { id: workItemId, assigneeIds: [mockAssignees[0].id] },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
new file mode 100644
index 00000000000..8017c46dea8
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -0,0 +1,222 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+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 WorkItemDescription from '~/work_items/components/work_item_description.vue';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import updateWorkItemWidgetsMutation from '~/work_items/graphql/update_work_item_widgets.mutation.graphql';
+import {
+ updateWorkItemWidgetsResponse,
+ workItemResponseFactory,
+ workItemQueryResponse,
+} from '../mock_data';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
+ return {
+ confirmAction: jest.fn(),
+ };
+});
+jest.mock('~/lib/utils/autosave');
+
+const workItemId = workItemQueryResponse.data.workItem.id;
+
+describe('WorkItemDescription', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemWidgetsResponse);
+
+ const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
+ const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+
+ const editDescription = (newText) => 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', {});
+
+ const createComponent = async ({
+ mutationHandler = mutationSuccessHandler,
+ canUpdate = true,
+ isEditing = false,
+ } = {}) => {
+ const workItemResponse = workItemResponseFactory({ canUpdate });
+ const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
+
+ const { id } = workItemQueryResponse.data.workItem;
+ wrapper = shallowMount(WorkItemDescription, {
+ apolloProvider: createMockApollo([
+ [workItemQuery, workItemResponseHandler],
+ [updateWorkItemWidgetsMutation, mutationHandler],
+ ]),
+ propsData: {
+ workItemId: id,
+ },
+ provide: {
+ fullPath: '/group/project',
+ },
+ stubs: {
+ MarkdownField,
+ },
+ });
+
+ await waitForPromises();
+
+ if (isEditing) {
+ findEditButton().vm.$emit('click');
+
+ await nextTick();
+ }
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ 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 () => {
+ await createComponent({
+ canUpdate: true,
+ });
+
+ findEditButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findMarkdownField().exists()).toBe(true);
+ });
+ });
+
+ describe('editing description', () => {
+ it('cancels when clicking cancel', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ clickCancel();
+
+ await nextTick();
+
+ expect(confirmAction).not.toHaveBeenCalled();
+ expect(findMarkdownField().exists()).toBe(false);
+ });
+
+ it('prompts for confirmation when clicking cancel after changes', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ editDescription('updated desc');
+
+ clickCancel();
+
+ await nextTick();
+
+ expect(confirmAction).toHaveBeenCalled();
+ });
+
+ it('calls update widgets mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ editDescription('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ descriptionWidget: {
+ description: 'updated desc',
+ },
+ },
+ });
+ });
+
+ it('tracks editing description', async () => {
+ await createComponent({
+ isEditing: true,
+ markdownPreviewPath: '/preview',
+ });
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_description',
+ property: 'type_Task',
+ });
+ });
+
+ it('emits error when mutation returns error', async () => {
+ const error = 'eror';
+
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdateWidgets: {
+ workItem: {},
+ errors: [error],
+ },
+ },
+ }),
+ });
+
+ editDescription('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
+
+ it('emits error when mutation fails', async () => {
+ const error = 'eror';
+
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
+ });
+
+ editDescription('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
+
+ it('autosaves description', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ editDescription('updated desc');
+
+ expect(updateDraft).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 aaabdbc82d9..d55ba318e46 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
@@ -29,7 +29,7 @@ describe('WorkItemDetailModal component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
- const createComponent = ({ workItemId = '1', error = false } = {}) => {
+ const createComponent = ({ workItemId = '1', issueGid = '2', error = false } = {}) => {
const apolloProvider = createMockApollo([
[
deleteWorkItemFromTaskMutation,
@@ -46,7 +46,7 @@ describe('WorkItemDetailModal component', () => {
wrapper = shallowMount(WorkItemDetailModal, {
apolloProvider,
- propsData: { workItemId },
+ propsData: { workItemId, issueGid },
data() {
return {
error,
@@ -67,6 +67,7 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
workItemId: '1',
+ workItemParentId: '2',
});
});
@@ -97,13 +98,6 @@ describe('WorkItemDetailModal component', () => {
expect(wrapper.emitted('close')).toBeTruthy();
});
- it('emits `workItemUpdated` event on updating work item', () => {
- createComponent();
- findWorkItemDetail().vm.$emit('workItemUpdated');
-
- expect(wrapper.emitted('workItemUpdated')).toBeTruthy();
- });
-
describe('delete work item', () => {
it('emits workItemDeleted and closes modal', async () => {
createComponent();
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
new file mode 100644
index 00000000000..774e9198992
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -0,0 +1,88 @@
+import Vue, { nextTick } from 'vue';
+import { GlBadge } 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 WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
+import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
+import { workItemHierarchyResponse, workItemHierarchyEmptyResponse } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+describe('WorkItemLinks', () => {
+ let wrapper;
+
+ const createComponent = async ({ response = workItemHierarchyResponse } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinks, {
+ apolloProvider: createMockApollo([
+ [getWorkItemLinksQuery, jest.fn().mockResolvedValue(response)],
+ ]),
+ propsData: { issuableId: 1 },
+ });
+
+ await waitForPromises();
+ };
+
+ const findToggleButton = () => wrapper.findByTestId('toggle-links');
+ const findLinksBody = () => wrapper.findByTestId('links-body');
+ const findEmptyState = () => wrapper.findByTestId('links-empty');
+ const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
+ const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
+
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('is expanded by default', () => {
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findLinksBody().exists()).toBe(true);
+ });
+
+ it('expands on click toggle button', async () => {
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findLinksBody().exists()).toBe(false);
+ });
+
+ describe('when no child links', () => {
+ beforeEach(async () => {
+ await createComponent({ response: workItemHierarchyEmptyResponse });
+ });
+
+ it('displays empty state if there are no children', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ describe('add link form', () => {
+ it('displays form on click add button and hides form on cancel', async () => {
+ expect(findEmptyState().exists()).toBe(true);
+
+ findToggleAddFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(true);
+
+ findAddLinksForm().vm.$emit('cancel');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(false);
+ });
+ });
+ });
+
+ it('renders all hierarchy widget children', () => {
+ expect(findLinksBody().exists()).toBe(true);
+
+ const children = wrapper.findAll('[data-testid="links-child"]');
+
+ expect(children).toHaveLength(4);
+ expect(children.at(0).findComponent(GlBadge).text()).toBe('Open');
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js
index 9e48f56d9e9..b379d1fc846 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -12,6 +12,7 @@ import {
STATE_CLOSED,
STATE_EVENT_CLOSE,
STATE_EVENT_REOPEN,
+ TRACKING_CATEGORY_SHOW,
} from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
@@ -81,15 +82,6 @@ describe('WorkItemState component', () => {
});
});
- it('emits updated event', async () => {
- createComponent();
-
- findItemState().vm.$emit('changed', STATE_CLOSED);
- await waitForPromises();
-
- expect(wrapper.emitted('updated')).toEqual([[]]);
- });
-
it('emits an error message when the mutation was unsuccessful', async () => {
createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') });
@@ -107,8 +99,8 @@ describe('WorkItemState component', () => {
findItemState().vm.$emit('changed', STATE_CLOSED);
await waitForPromises();
- expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_state', {
- category: 'workItems:show',
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_state', {
+ category: TRACKING_CATEGORY_SHOW,
label: 'item_state',
property: 'type_Task',
});
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index 19b56362ac0..a48449bb636 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -6,8 +6,9 @@ import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ItemTitle from '~/work_items/components/item_title.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
-import { i18n } from '~/work_items/constants';
+import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
describe('WorkItemTitle component', () => {
@@ -19,14 +20,18 @@ describe('WorkItemTitle component', () => {
const findItemTitle = () => wrapper.findComponent(ItemTitle);
- const createComponent = ({ mutationHandler = mutationSuccessHandler } = {}) => {
+ const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, {
- apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
+ apolloProvider: createMockApollo([
+ [updateWorkItemMutation, mutationHandler],
+ [updateWorkItemTaskMutation, mutationHandler],
+ ]),
propsData: {
workItemId: id,
workItemTitle: title,
workItemType: workItemType.name,
+ workItemParentId,
},
});
};
@@ -57,13 +62,25 @@ describe('WorkItemTitle component', () => {
});
});
- it('emits updated event', async () => {
- createComponent();
+ it('calls WorkItemTaskUpdate if passed workItemParentId prop', () => {
+ const title = 'new title!';
+ const workItemParentId = '1234';
- findItemTitle().vm.$emit('title-changed', 'new title');
- await waitForPromises();
+ createComponent({
+ workItemParentId,
+ });
- expect(wrapper.emitted('updated')).toEqual([[]]);
+ findItemTitle().vm.$emit('title-changed', title);
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemParentId,
+ taskData: {
+ id: workItemQueryResponse.data.workItem.id,
+ title,
+ },
+ },
+ });
});
it('does not call a mutation when the title has not changed', () => {
@@ -91,8 +108,8 @@ describe('WorkItemTitle component', () => {
findItemTitle().vm.$emit('title-changed', 'new title');
await waitForPromises();
- expect(trackingSpy).toHaveBeenCalledWith('workItems:show', 'updated_title', {
- category: 'workItems:show',
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_title', {
+ category: TRACKING_CATEGORY_SHOW,
label: 'item_title',
property: 'type_Task',
});
diff --git a/spec/frontend/work_items/components/work_item_weight_spec.js b/spec/frontend/work_items/components/work_item_weight_spec.js
new file mode 100644
index 00000000000..80a1d032ad7
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_weight_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
+
+describe('WorkItemAssignees component', () => {
+ let wrapper;
+
+ const createComponent = ({ weight, hasIssueWeightsFeature = true } = {}) => {
+ wrapper = shallowMount(WorkItemWeight, {
+ propsData: {
+ weight,
+ },
+ provide: {
+ hasIssueWeightsFeature,
+ },
+ });
+ };
+
+ describe('weight licensed feature', () => {
+ describe.each`
+ description | hasIssueWeightsFeature | exists
+ ${'when available'} | ${true} | ${true}
+ ${'when not available'} | ${false} | ${false}
+ `('$description', ({ hasIssueWeightsFeature, exists }) => {
+ it(hasIssueWeightsFeature ? 'renders component' : 'does not render component', () => {
+ createComponent({ hasIssueWeightsFeature });
+
+ expect(wrapper.find('div').exists()).toBe(exists);
+ });
+ });
+ });
+
+ describe('weight text', () => {
+ describe.each`
+ description | weight | text
+ ${'renders 1'} | ${1} | ${'1'}
+ ${'renders 0'} | ${0} | ${'0'}
+ ${'renders None'} | ${null} | ${'None'}
+ ${'renders None'} | ${undefined} | ${'None'}
+ `('when weight is $weight', ({ description, weight, text }) => {
+ it(description, () => {
+ createComponent({ weight });
+
+ expect(wrapper.text()).toContain(text);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index f3483550013..bf3f4e1364d 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -15,6 +15,15 @@ export const workItemQueryResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetDescription',
+ type: 'DESCRIPTION',
+ description: 'some **great** text',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ },
+ ],
},
},
};
@@ -38,11 +47,53 @@ export const updateWorkItemMutationResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ widgets: [],
},
},
},
};
+export const workItemResponseFactory = ({ canUpdate } = {}) => ({
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Updated title',
+ state: 'OPEN',
+ description: 'description',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ },
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: canUpdate,
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetDescription',
+ type: 'DESCRIPTION',
+ description: 'some **great** text',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ },
+ ],
+ },
+ },
+});
+
+export const updateWorkItemWidgetsResponse = {
+ data: {
+ workItemUpdateWidgets: {
+ workItem: {
+ id: 1234,
+ },
+ errors: [],
+ },
+ },
+};
+
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -77,6 +128,7 @@ export const createWorkItemMutationResponse = {
deleteWorkItem: false,
updateWorkItem: false,
},
+ widgets: [],
},
},
},
@@ -124,3 +176,102 @@ export const workItemTitleSubscriptionResponse = {
},
},
};
+
+export const workItemHierarchyEmptyResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ widgets: [
+ {
+ type: 'DESCRIPTION',
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ children: {
+ nodes: [],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+
+export const workItemHierarchyResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/1',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ __typename: 'WorkItemType',
+ },
+ title: 'New title',
+ widgets: [
+ {
+ type: 'DESCRIPTION',
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ type: 'HIERARCHY',
+ parent: null,
+ children: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/2',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'xyz',
+ state: 'OPEN',
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/3',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'abc',
+ state: 'CLOSED',
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'bar',
+ state: 'OPEN',
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/5',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ __typename: 'WorkItemType',
+ },
+ title: 'foobar',
+ state: 'OPEN',
+ __typename: 'WorkItem',
+ },
+ ],
+ __typename: 'WorkItemConnection',
+ },
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 9f87655175c..b9724034cb4 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -5,11 +5,15 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
+import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
+import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
+import { temporaryConfig } from '~/work_items/graphql/provider';
import { workItemTitleSubscriptionResponse, workItemQueryResponse } from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -24,18 +28,34 @@ describe('WorkItemDetail component', () => {
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
+ const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
+ const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
+ const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight);
const createComponent = ({
workItemId = workItemQueryResponse.data.workItem.id,
handler = successHandler,
subscriptionHandler = initialSubscriptionHandler,
+ workItemsMvc2Enabled = false,
+ includeWidgets = false,
} = {}) => {
wrapper = shallowMount(WorkItemDetail, {
- apolloProvider: createMockApollo([
- [workItemQuery, handler],
- [workItemTitleSubscription, subscriptionHandler],
- ]),
+ apolloProvider: createMockApollo(
+ [
+ [workItemQuery, handler],
+ [workItemTitleSubscription, subscriptionHandler],
+ ],
+ {},
+ {
+ typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
+ },
+ ),
propsData: { workItemId },
+ provide: {
+ glFeatures: {
+ workItemsMvc2: workItemsMvc2Enabled,
+ },
+ },
});
};
@@ -78,6 +98,22 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('description', () => {
+ it('does not show description widget if loading description fails', () => {
+ createComponent();
+
+ expect(findWorkItemDescription().exists()).toBe(false);
+ });
+
+ it('shows description widget if description loads', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findWorkItemDescription().exists()).toBe(true);
+ });
+ });
+
it('shows an error message when the work item query was unsuccessful', async () => {
const errorHandler = jest.fn().mockRejectedValue('Oops');
createComponent({ handler: errorHandler });
@@ -105,17 +141,64 @@ describe('WorkItemDetail component', () => {
});
});
- it('emits workItemUpdated event when fields updated', async () => {
- createComponent();
+ describe('when work_items_mvc_2 feature flag is enabled', () => {
+ it('renders assignees component when assignees widget is returned from the API', async () => {
+ createComponent({
+ workItemsMvc2Enabled: true,
+ includeWidgets: true,
+ });
+ await waitForPromises();
- await waitForPromises();
+ expect(findWorkItemAssignees().exists()).toBe(true);
+ });
- findWorkItemState().vm.$emit('updated');
+ it('does not render assignees component when assignees widget is not returned from the API', async () => {
+ createComponent({
+ workItemsMvc2Enabled: true,
+ includeWidgets: false,
+ });
+ await waitForPromises();
- expect(wrapper.emitted('workItemUpdated')).toEqual([[]]);
+ expect(findWorkItemAssignees().exists()).toBe(false);
+ });
+ });
- findWorkItemTitle().vm.$emit('updated');
+ it('does not render assignees component when assignees feature flag is disabled', async () => {
+ createComponent();
+ await waitForPromises();
- expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]);
+ expect(findWorkItemAssignees().exists()).toBe(false);
+ });
+
+ describe('weight widget', () => {
+ describe('when work_items_mvc_2 feature flag is enabled', () => {
+ describe.each`
+ description | includeWidgets | exists
+ ${'when widget is returned from API'} | ${true} | ${true}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ includeWidgets, exists }) => {
+ it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => {
+ createComponent({ includeWidgets, workItemsMvc2Enabled: true });
+ await waitForPromises();
+
+ expect(findWorkItemWeight().exists()).toBe(exists);
+ });
+ });
+ });
+
+ describe('when work_items_mvc_2 feature flag is disabled', () => {
+ describe.each`
+ description | includeWidgets | exists
+ ${'when widget is returned from API'} | ${true} | ${false}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ includeWidgets, exists }) => {
+ it(`${includeWidgets ? 'renders' : 'does not render'} weight component`, async () => {
+ createComponent({ includeWidgets, workItemsMvc2Enabled: false });
+ await waitForPromises();
+
+ expect(findWorkItemWeight().exists()).toBe(exists);
+ });
+ });
+ });
});
});
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 85096392e84..3c5da94114e 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -11,6 +11,7 @@ import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graph
import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
@@ -52,6 +53,7 @@ describe('Work items root component', () => {
expect(findWorkItemDetail().props()).toEqual({
workItemId: 'gid://gitlab/WorkItem/1',
+ workItemParentId: null,
});
});