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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/emoji.js5
-rw-r--r--spec/frontend/__helpers__/local_storage_helper.js4
-rw-r--r--spec/frontend/__helpers__/mock_window_location_helper.js4
-rw-r--r--spec/frontend/__helpers__/test_apollo_link.js46
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap4
-rw-r--r--spec/frontend/api/projects_api_spec.js62
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js2
-rw-r--r--spec/frontend/autosave_spec.js8
-rw-r--r--spec/frontend/batch_comments/components/review_bar_spec.js42
-rw-r--r--spec/frontend/batch_comments/create_batch_comments_store.js15
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js3
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js33
-rw-r--r--spec/frontend/boards/board_list_deprecated_spec.js274
-rw-r--r--spec/frontend/boards/board_new_issue_deprecated_spec.js211
-rw-r--r--spec/frontend/boards/boards_store_spec.js1013
-rw-r--r--spec/frontend/boards/components/board_add_new_column_spec.js1
-rw-r--r--spec/frontend/boards/components/board_app_spec.js54
-rw-r--r--spec/frontend/boards/components/board_card_deprecated_spec.js219
-rw-r--r--spec/frontend/boards/components/board_card_layout_deprecated_spec.js158
-rw-r--r--spec/frontend/boards/components/board_column_deprecated_spec.js106
-rw-r--r--spec/frontend/boards/components/board_content_spec.js75
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js22
-rw-r--r--spec/frontend/boards/components/board_list_header_deprecated_spec.js174
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js142
-rw-r--r--spec/frontend/boards/components/boards_selector_deprecated_spec.js214
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js64
-rw-r--r--spec/frontend/boards/issue_card_deprecated_spec.js332
-rw-r--r--spec/frontend/boards/issue_spec.js162
-rw-r--r--spec/frontend/boards/list_spec.js230
-rw-r--r--spec/frontend/boards/mock_data.js89
-rw-r--r--spec/frontend/boards/project_select_deprecated_spec.js263
-rw-r--r--spec/frontend/boards/stores/actions_spec.js23
-rw-r--r--spec/frontend/boards/stores/getters_spec.js16
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js6
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap18
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap26
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js42
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap9
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js10
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js193
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js37
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js37
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js112
-rw-r--r--spec/frontend/content_editor/extensions/blockquote_spec.js19
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js17
-rw-r--r--spec/frontend/content_editor/markdown_processing_examples.js9
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec.js5
-rw-r--r--spec/frontend/content_editor/services/mark_utils_spec.js38
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js1008
-rw-r--r--spec/frontend/content_editor/services/markdown_sourcemap_spec.js81
-rw-r--r--spec/frontend/content_editor/test_utils.js4
-rw-r--r--spec/frontend/cycle_analytics/banner_spec.js47
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js28
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js19
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js61
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js11
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js26
-rw-r--r--spec/frontend/deploy_freeze/helpers.js2
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js45
-rw-r--r--spec/frontend/deploy_freeze/store/mutations_spec.js6
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap4
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js20
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap8
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap8
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js27
-rw-r--r--spec/frontend/design_management/pages/index_spec.js17
-rw-r--r--spec/frontend/design_management/utils/design_management_utils_spec.js8
-rw-r--r--spec/frontend/diffs/components/app_spec.js22
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js38
-rw-r--r--spec/frontend/diffs/create_diffs_store.js6
-rw-r--r--spec/frontend/diffs/store/actions_spec.js7
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js6
-rw-r--r--spec/frontend/diffs/utils/preferences_spec.js32
-rw-r--r--spec/frontend/dropzone_input_spec.js48
-rw-r--r--spec/frontend/emoji/index_spec.js13
-rw-r--r--spec/frontend/emoji/support/unicode_support_map_spec.js6
-rw-r--r--spec/frontend/environments/edit_environment_spec.js52
-rw-r--r--spec/frontend/environments/environment_form_spec.js48
-rw-r--r--spec/frontend/environments/environment_item_spec.js7
-rw-r--r--spec/frontend/environments/environment_table_spec.js11
-rw-r--r--spec/frontend/environments/environments_app_spec.js51
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js8
-rw-r--r--spec/frontend/environments/environments_folder_view_spec.js1
-rw-r--r--spec/frontend/environments/folder/environments_folder_view_spec.js1
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js80
-rw-r--r--spec/frontend/error_tracking_settings/mock.js4
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking_settings/store/mutation_spec.js8
-rw-r--r--spec/frontend/error_tracking_settings/utils_spec.js2
-rw-r--r--spec/frontend/experimentation/utils_spec.js44
-rw-r--r--spec/frontend/filtered_search/services/recent_searches_service_spec.js6
-rw-r--r--spec/frontend/fixtures/api_markdown.yml114
-rw-r--r--spec/frontend/fixtures/freeze_period.rb9
-rw-r--r--spec/frontend/fixtures/runner.rb47
-rw-r--r--spec/frontend/fixtures/startup_css.rb15
-rw-r--r--spec/frontend/fixtures/static/pipeline_graph.html24
-rw-r--r--spec/frontend/fixtures/timezones.rb22
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js2
-rw-r--r--spec/frontend/groups/components/app_spec.js7
-rw-r--r--spec/frontend/groups/components/groups_spec.js13
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js77
-rw-r--r--spec/frontend/groups/components/item_stats_spec.js30
-rw-r--r--spec/frontend/header_search/components/app_spec.js159
-rw-r--r--spec/frontend/header_search/components/header_search_default_items_spec.js81
-rw-r--r--spec/frontend/header_search/components/header_search_scoped_items_spec.js61
-rw-r--r--spec/frontend/header_search/mock_data.js83
-rw-r--r--spec/frontend/header_search/store/actions_spec.js28
-rw-r--r--spec/frontend/header_search/store/getters_spec.js211
-rw-r--r--spec/frontend/header_search/store/mutations_spec.js20
-rw-r--r--spec/frontend/header_spec.js4
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js33
-rw-r--r--spec/frontend/ide/services/terminals_spec.js51
-rw-r--r--spec/frontend/ide/utils_spec.js8
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js90
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js59
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js8
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js29
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js38
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js4
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js2
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js29
-rw-r--r--spec/frontend/invite_members/components/import_a_project_modal_spec.js167
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js4
-rw-r--r--spec/frontend/invite_members/components/project_select_spec.js105
-rw-r--r--spec/frontend/invite_members/mock_data/api_response_data.js13
-rw-r--r--spec/frontend/issue_show/components/app_spec.js44
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js137
-rw-r--r--spec/frontend/issues_list/mock_data.js11
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap36
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js126
-rw-r--r--spec/frontend/jobs/components/table/cells/duration_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js)0
-rw-r--r--spec/frontend/jobs/components/table/cells/job_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js)0
-rw-r--r--spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js)0
-rw-r--r--spec/frontend/jobs/mock_data.js182
-rw-r--r--spec/frontend/learn_gitlab/track_learn_gitlab_spec.js21
-rw-r--r--spec/frontend/lib/apollo/instrumentation_link_spec.js54
-rw-r--r--spec/frontend/lib/dompurify_spec.js25
-rw-r--r--spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap16
-rw-r--r--spec/frontend/lib/logger/hello_deferred_spec.js17
-rw-r--r--spec/frontend/lib/logger/hello_spec.js20
-rw-r--r--spec/frontend/lib/logger/index_spec.js23
-rw-r--r--spec/frontend/lib/utils/accessor_spec.js65
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js120
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js15
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js19
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js23
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js8
-rw-r--r--spec/frontend/merge_request_tabs_spec.js82
-rw-r--r--spec/frontend/milestones/stores/mutations_spec.js58
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap4
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js140
-rw-r--r--spec/frontend/notebook/cells/output/html_sanitize_fixtures.js3
-rw-r--r--spec/frontend/notebook/index_spec.js31
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js13
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js64
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js (renamed from spec/frontend/notes/old_notes_spec.js)7
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js86
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js58
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js48
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js52
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js55
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js48
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap68
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js273
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js217
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js128
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js71
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js48
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js4
-rw-r--r--spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js2
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap4
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap604
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap (renamed from spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap)16
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js38
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js (renamed from spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js)6
-rw-r--r--spec/frontend/pages/projects/new/components/new_project_url_select_spec.js122
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js5
-rw-r--r--spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js19
-rw-r--r--spec/frontend/pipeline_editor/components/editor/text_editor_spec.js9
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js9
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js18
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js8
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js13
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js51
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js132
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js17
-rw-r--r--spec/frontend/pipelines/__snapshots__/utils_spec.js.snap (renamed from spec/frontend/pipelines/__snapshots__/parsing_utils_spec.js.snap)0
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js5
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js36
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js44
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_mock_data.js3812
-rw-r--r--spec/frontend/pipelines/header_component_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js12
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_source_token_spec.js3
-rw-r--r--spec/frontend/pipelines/utils_spec.js (renamed from spec/frontend/pipelines/parsing_utils_spec.js)2
-rw-r--r--spec/frontend/pipelines_spec.js17
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js25
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js2
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js23
-rw-r--r--spec/frontend/projects/storage_counter/components/app_spec.js150
-rw-r--r--spec/frontend/projects/storage_counter/components/storage_table_spec.js62
-rw-r--r--spec/frontend/projects/storage_counter/mock_data.js109
-rw-r--r--spec/frontend/projects/storage_counter/utils_spec.js17
-rw-r--r--spec/frontend/projects/terraform_notification/terraform_notification_spec.js70
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js24
-rw-r--r--spec/frontend/repository/components/blob_viewers/image_viewer_spec.js25
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js7
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js97
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js33
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js2
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js14
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js239
-rw-r--r--spec/frontend/runner/mock_data.js16
-rw-r--r--spec/frontend/search/highlight_blob_search_result_spec.js2
-rw-r--r--spec/frontend/search/store/actions_spec.js8
-rw-r--r--spec/frontend/search/store/utils_spec.js2
-rw-r--r--spec/frontend/shortcuts_spec.js3
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js2
-rw-r--r--spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js10
-rw-r--r--spec/frontend/sidebar/sidebar_labels_spec.js2
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js17
-rw-r--r--spec/frontend/sidebar/sidebar_store_spec.js39
-rw-r--r--spec/frontend/sidebar/track_invite_members_spec.js2
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap1
-rw-r--r--spec/frontend/tracking_spec.js149
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap37
-rw-r--r--spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js49
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js176
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js74
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap23
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js54
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_button_spec.js91
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js79
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js152
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js189
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js85
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/getters_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js115
-rw-r--r--spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js137
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js17
-rw-r--r--spec/frontend/zen_mode_spec.js6
271 files changed, 8337 insertions, 10276 deletions
diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js
index 9f9134f6f63..a64135601ae 100644
--- a/spec/frontend/__helpers__/emoji.js
+++ b/spec/frontend/__helpers__/emoji.js
@@ -49,6 +49,11 @@ export const emojiFixtureMap = {
unicodeVersion: '5.1',
description: 'white medium star',
},
+ xss: {
+ moji: '<img src=x onerror=prompt(1)>',
+ unicodeVersion: '5.1',
+ description: 'xss',
+ },
};
export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
diff --git a/spec/frontend/__helpers__/local_storage_helper.js b/spec/frontend/__helpers__/local_storage_helper.js
index 21749fd8070..cf75b0b53fe 100644
--- a/spec/frontend/__helpers__/local_storage_helper.js
+++ b/spec/frontend/__helpers__/local_storage_helper.js
@@ -2,9 +2,7 @@
* Manage the instance of a custom `window.localStorage`
*
* This only encapsulates the setup / teardown logic so that it can easily be
- * reused with different implementations (i.e. a spy or a [fake][1])
- *
- * [1]: https://stackoverflow.com/a/41434763/1708147
+ * reused with different implementations (i.e. a spy or a fake)
*
* @param {() => any} fn Function that returns the object to use for localStorage
*/
diff --git a/spec/frontend/__helpers__/mock_window_location_helper.js b/spec/frontend/__helpers__/mock_window_location_helper.js
index 3755778e5c1..14082857053 100644
--- a/spec/frontend/__helpers__/mock_window_location_helper.js
+++ b/spec/frontend/__helpers__/mock_window_location_helper.js
@@ -2,9 +2,7 @@
* Manage the instance of a custom `window.location`
*
* This only encapsulates the setup / teardown logic so that it can easily be
- * reused with different implementations (i.e. a spy or a [fake][1])
- *
- * [1]: https://stackoverflow.com/a/41434763/1708147
+ * reused with different implementations (i.e. a spy or a fake)
*
* @param {() => any} fn Function that returns the object to use for window.location
*/
diff --git a/spec/frontend/__helpers__/test_apollo_link.js b/spec/frontend/__helpers__/test_apollo_link.js
new file mode 100644
index 00000000000..dde3a4e99bb
--- /dev/null
+++ b/spec/frontend/__helpers__/test_apollo_link.js
@@ -0,0 +1,46 @@
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import { ApolloClient } from 'apollo-client';
+import { ApolloLink } from 'apollo-link';
+import gql from 'graphql-tag';
+
+const FOO_QUERY = gql`
+ query {
+ foo
+ }
+`;
+
+/**
+ * This function returns a promise that resolves to the final operation after
+ * running an ApolloClient query with the given ApolloLink
+ *
+ * @typedef {Object} TestApolloLinkOptions
+ * @property {Object} context the default context object sent along the ApolloLink chain
+ *
+ * @param {ApolloLink} subjectLink the ApolloLink which is under test
+ * @param {TestApolloLinkOptions} options contains options to send a long with the query
+ *
+ * @returns Promise resolving to the resulting operation after running the subjectLink
+ */
+export const testApolloLink = (subjectLink, options = {}) =>
+ new Promise((resolve) => {
+ const { context = {} } = options;
+
+ // Use the terminating link to capture the final operation and resolve with this.
+ const terminatingLink = new ApolloLink((operation) => {
+ resolve(operation);
+
+ return null;
+ });
+
+ const client = new ApolloClient({
+ link: ApolloLink.from([subjectLink, terminatingLink]),
+ // cache is a required option
+ cache: new InMemoryCache(),
+ });
+
+ // Trigger a query so the ApolloLink chain will be executed.
+ client.query({
+ context,
+ query: FOO_QUERY,
+ });
+ });
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
index 3a374084dbc..ddb188edb10 100644
--- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -51,10 +51,14 @@ exports[`Alert integration settings form default state should match the default
<gl-dropdown-stub
block="true"
category="primary"
+ clearalltext="Clear all"
data-qa-selector="incident_templates_dropdown"
headertext=""
hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
id="alert-integration-settings-issue-template"
+ showhighlighteditemstitle="true"
size="medium"
text="selecte_tmpl"
variant="default"
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
new file mode 100644
index 00000000000..8f40b557e1f
--- /dev/null
+++ b/spec/frontend/api/projects_api_spec.js
@@ -0,0 +1,62 @@
+import MockAdapter from 'axios-mock-adapter';
+import * as projectsApi from '~/api/projects_api';
+import axios from '~/lib/utils/axios_utils';
+
+describe('~/api/projects_api.js', () => {
+ let mock;
+ let originalGon;
+
+ const projectId = 1;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ originalGon = window.gon;
+ window.gon = { api_version: 'v7' };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ window.gon = originalGon;
+ });
+
+ describe('getProjects', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ });
+
+ it('retrieves projects from the correct URL and returns them in the response data', () => {
+ const expectedUrl = '/api/v7/projects.json';
+ const expectedParams = { params: { per_page: 20, search: '', simple: true } };
+ const expectedProjects = [{ name: 'project 1' }];
+ const query = '';
+ const options = {};
+
+ mock.onGet(expectedUrl).reply(200, { data: expectedProjects });
+
+ return projectsApi.getProjects(query, options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
+ expect(data.data).toEqual(expectedProjects);
+ });
+ });
+ });
+
+ describe('importProjectMembers', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'post');
+ });
+
+ it('posts to the correct URL and returns the response message', () => {
+ const targetId = 2;
+ const expectedUrl = '/api/v7/projects/1/import_project_members/2';
+ const expectedMessage = 'Successfully imported';
+
+ mock.onPost(expectedUrl).replyOnce(200, expectedMessage);
+
+ return projectsApi.importProjectMembers(projectId, targetId).then(({ data }) => {
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl);
+ expect(data).toEqual(expectedMessage);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
index b77def195b6..2dcc537809f 100644
--- a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
@@ -78,7 +78,7 @@ describe('RecoveryCodes', () => {
it('fires Snowplow event', () => {
expect(findProceedButton().attributes()).toMatchObject({
- 'data-track-event': 'click_button',
+ 'data-track-action': 'click_button',
'data-track-label': '2fa_recovery_codes_proceed_button',
});
});
diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js
index bbdf3c6f91d..c881e0f9794 100644
--- a/spec/frontend/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -15,28 +15,28 @@ describe('Autosave', () => {
describe('class constructor', () => {
beforeEach(() => {
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
jest.spyOn(Autosave.prototype, 'restore').mockImplementation(() => {});
});
it('should set .isLocalStorageAvailable', () => {
autosave = new Autosave(field, key);
- expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(AccessorUtilities.canUseLocalStorage).toHaveBeenCalled();
expect(autosave.isLocalStorageAvailable).toBe(true);
});
it('should set .isLocalStorageAvailable if fallbackKey is passed', () => {
autosave = new Autosave(field, key, fallbackKey);
- expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(AccessorUtilities.canUseLocalStorage).toHaveBeenCalled();
expect(autosave.isLocalStorageAvailable).toBe(true);
});
it('should set .isLocalStorageAvailable if lockVersion is passed', () => {
autosave = new Autosave(field, key, null, lockVersion);
- expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(AccessorUtilities.canUseLocalStorage).toHaveBeenCalled();
expect(autosave.isLocalStorageAvailable).toBe(true);
});
});
diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js
new file mode 100644
index 00000000000..f50db6ab210
--- /dev/null
+++ b/spec/frontend/batch_comments/components/review_bar_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount } from '@vue/test-utils';
+import ReviewBar from '~/batch_comments/components/review_bar.vue';
+import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '~/batch_comments/constants';
+import createStore from '../create_batch_comments_store';
+
+describe('Batch comments review bar component', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ store = createStore();
+
+ wrapper = shallowMount(ReviewBar, {
+ store,
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ document.body.className = '';
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('it adds review-bar-visible class to body when review bar is mounted', async () => {
+ expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
+
+ createComponent();
+
+ expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true);
+ });
+
+ it('it removes review-bar-visible class to body when review bar is destroyed', async () => {
+ createComponent();
+
+ wrapper.destroy();
+
+ expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
+ });
+});
diff --git a/spec/frontend/batch_comments/create_batch_comments_store.js b/spec/frontend/batch_comments/create_batch_comments_store.js
new file mode 100644
index 00000000000..10dc6fe196e
--- /dev/null
+++ b/spec/frontend/batch_comments/create_batch_comments_store.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import batchCommentsModule from '~/batch_comments/stores/modules/batch_comments';
+import notesModule from '~/notes/stores/modules';
+
+Vue.use(Vuex);
+
+export default function createDiffsStore() {
+ return new Vuex.Store({
+ modules: {
+ notes: notesModule(),
+ batchComments: batchCommentsModule(),
+ },
+ });
+}
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index 604104bb31f..93406db2675 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -11,6 +11,7 @@ describe('iPython notebook renderer', () => {
let mock;
const endpoint = 'test';
+ const relativeRawPath = '';
const mockNotebook = {
cells: [
{
@@ -27,7 +28,7 @@ describe('iPython notebook renderer', () => {
};
const mountComponent = () => {
- wrapper = shallowMount(component, { propsData: { endpoint } });
+ wrapper = shallowMount(component, { propsData: { endpoint, relativeRawPath } });
};
const findLoading = () => wrapper.find(GlLoadingIcon);
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 7d3ecc773a6..e0446811f64 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -2,6 +2,7 @@ import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
@@ -44,6 +45,7 @@ describe('Board card component', () => {
const findEpicBadgeProgress = () => wrapper.findByTestId('epic-progress');
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
+ const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
@@ -72,6 +74,9 @@ describe('Board card component', () => {
GlLabel: true,
GlLoadingIcon: true,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
mocks: {
$apollo: {
queries: {
@@ -122,6 +127,10 @@ describe('Board card component', () => {
expect(wrapper.find('.confidential-icon').exists()).toBe(false);
});
+ it('does not render hidden issue icon', () => {
+ expect(findHiddenIssueIcon().exists()).toBe(false);
+ });
+
it('renders issue ID with #', () => {
expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.iid}`);
});
@@ -184,6 +193,30 @@ describe('Board card component', () => {
});
});
+ describe('hidden issue', () => {
+ beforeEach(() => {
+ wrapper.setProps({
+ item: {
+ ...wrapper.props('item'),
+ hidden: true,
+ },
+ });
+ });
+
+ it('renders hidden issue icon', () => {
+ expect(findHiddenIssueIcon().exists()).toBe(true);
+ });
+
+ it('displays a tooltip which explains the meaning of the icon', () => {
+ const tooltip = getBinding(findHiddenIssueIcon().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(findHiddenIssueIcon().attributes('title')).toBe(
+ 'This issue is hidden because its author has been banned',
+ );
+ });
+ });
+
describe('with assignee', () => {
describe('with avatar', () => {
beforeEach(() => {
diff --git a/spec/frontend/boards/board_list_deprecated_spec.js b/spec/frontend/boards/board_list_deprecated_spec.js
deleted file mode 100644
index b71564f7858..00000000000
--- a/spec/frontend/boards/board_list_deprecated_spec.js
+++ /dev/null
@@ -1,274 +0,0 @@
-/* global List */
-/* global ListIssue */
-import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import BoardList from '~/boards/components/board_list_deprecated.vue';
-import eventHub from '~/boards/eventhub';
-import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import { listObj, boardsMockInterceptor } from './mock_data';
-
-const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {
- const el = document.createElement('div');
-
- document.body.appendChild(el);
- const mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- boardsStore.create();
-
- const BoardListComp = Vue.extend(BoardList);
- const list = new List({ ...listObj, ...listProps });
- const issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- ...listIssueProps,
- });
- if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
- list.issuesSize = 1;
- }
- list.issues.push(issue);
-
- const component = new BoardListComp({
- el,
- store,
- propsData: {
- disabled: false,
- list,
- issues: list.issues,
- ...componentProps,
- },
- provide: {
- groupId: null,
- rootPath: '/',
- },
- }).$mount();
-
- Vue.nextTick(() => {
- done();
- });
-
- return { component, mock };
-};
-
-describe('Board list component', () => {
- let mock;
- let component;
- let getIssues;
- function generateIssues(compWrapper) {
- for (let i = 1; i < 20; i += 1) {
- const issue = { ...compWrapper.list.issues[0] };
- issue.id += i;
- compWrapper.list.issues.push(issue);
- }
- }
-
- describe('When Expanded', () => {
- beforeEach((done) => {
- getIssues = jest.spyOn(List.prototype, 'getIssues').mockReturnValue(new Promise(() => {}));
- ({ mock, component } = createComponent({ done }));
- });
-
- afterEach(() => {
- mock.restore();
- component.$destroy();
- });
-
- it('loads first page of issues', () => {
- return waitForPromises().then(() => {
- expect(getIssues).toHaveBeenCalled();
- });
- });
-
- it('renders component', () => {
- expect(component.$el.classList.contains('board-list-component')).toBe(true);
- });
-
- it('renders loading icon', () => {
- component.list.loading = true;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
- });
- });
-
- it('renders issues', () => {
- expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
- });
-
- it('sets data attribute with issue id', () => {
- expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
- });
-
- it('shows new issue form', () => {
- component.toggleForm();
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
-
- expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
- });
- });
-
- it('shows new issue form after eventhub event', () => {
- eventHub.$emit(`toggle-issue-form-${component.list.id}`);
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
-
- expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
- });
- });
-
- it('does not show new issue form for closed list', () => {
- component.list.type = 'closed';
- component.toggleForm();
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
- });
- });
-
- it('shows count list item', () => {
- component.showCount = true;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
-
- expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
- 'Showing all issues',
- );
- });
- });
-
- it('sets data attribute with invalid id', () => {
- component.showCount = true;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
- '-1',
- );
- });
- });
-
- it('shows how many more issues to load', () => {
- component.showCount = true;
- component.list.issuesSize = 20;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
- 'Showing 1 of 20 issues',
- );
- });
- });
-
- it('loads more issues after scrolling', () => {
- jest.spyOn(component.list, 'nextPage').mockImplementation(() => {});
- generateIssues(component);
- component.$refs.list.dispatchEvent(new Event('scroll'));
-
- return waitForPromises().then(() => {
- expect(component.list.nextPage).toHaveBeenCalled();
- });
- });
-
- it('does not load issues if already loading', () => {
- component.list.nextPage = jest
- .spyOn(component.list, 'nextPage')
- .mockReturnValue(new Promise(() => {}));
-
- component.onScroll();
- component.onScroll();
-
- return waitForPromises().then(() => {
- expect(component.list.nextPage).toHaveBeenCalledTimes(1);
- });
- });
-
- it('shows loading more spinner', () => {
- component.showCount = true;
- component.list.loadingMore = true;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
- });
- });
- });
-
- describe('When Collapsed', () => {
- beforeEach((done) => {
- getIssues = jest.spyOn(List.prototype, 'getIssues').mockReturnValue(new Promise(() => {}));
- ({ mock, component } = createComponent({
- done,
- listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
- }));
- generateIssues(component);
- component.scrollHeight = jest.spyOn(component, 'scrollHeight').mockReturnValue(0);
- });
-
- afterEach(() => {
- mock.restore();
- component.$destroy();
- });
-
- it('does not load all issues', () => {
- return waitForPromises().then(() => {
- // Initial getIssues from list constructor
- expect(getIssues).toHaveBeenCalledTimes(1);
- });
- });
- });
-
- describe('max issue count warning', () => {
- beforeEach((done) => {
- ({ mock, component } = createComponent({
- done,
- listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
- }));
- });
-
- afterEach(() => {
- mock.restore();
- component.$destroy();
- });
-
- describe('when issue count exceeds max issue count', () => {
- it('sets background to bg-danger-100', () => {
- component.list.issuesSize = 4;
- component.list.maxIssueCount = 3;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.bg-danger-100')).not.toBeNull();
- });
- });
- });
-
- describe('when list issue count does NOT exceed list max issue count', () => {
- it('does not sets background to bg-danger-100', () => {
- component.list.issuesSize = 2;
- component.list.maxIssueCount = 3;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
- });
- });
- });
-
- describe('when list max issue count is 0', () => {
- it('does not sets background to bg-danger-100', () => {
- component.list.maxIssueCount = 0;
-
- return Vue.nextTick().then(() => {
- expect(component.$el.querySelector('.bg-danger-100')).toBeNull();
- });
- });
- });
- });
-});
diff --git a/spec/frontend/boards/board_new_issue_deprecated_spec.js b/spec/frontend/boards/board_new_issue_deprecated_spec.js
deleted file mode 100644
index 3beaf870bf5..00000000000
--- a/spec/frontend/boards/board_new_issue_deprecated_spec.js
+++ /dev/null
@@ -1,211 +0,0 @@
-/* global List */
-
-import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import boardNewIssue from '~/boards/components/board_new_issue_deprecated.vue';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-
-import '~/boards/models/list';
-import { listObj, boardsMockInterceptor } from './mock_data';
-
-Vue.use(Vuex);
-
-describe('Issue boards new issue form', () => {
- let wrapper;
- let vm;
- let list;
- let mock;
- let newIssueMock;
- const promiseReturn = {
- data: {
- iid: 100,
- },
- };
-
- const submitIssue = () => {
- const dummySubmitEvent = {
- preventDefault() {},
- };
- wrapper.vm.$refs.submitButton = wrapper.find({ ref: 'submitButton' });
- return wrapper.vm.submit(dummySubmitEvent);
- };
-
- beforeEach(() => {
- const BoardNewIssueComp = Vue.extend(boardNewIssue);
-
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
-
- boardsStore.create();
-
- list = new List(listObj);
-
- newIssueMock = Promise.resolve(promiseReturn);
- jest.spyOn(list, 'newIssue').mockImplementation(() => newIssueMock);
-
- const store = new Vuex.Store({
- getters: { isGroupBoard: () => false },
- });
-
- wrapper = mount(BoardNewIssueComp, {
- propsData: {
- disabled: false,
- list,
- },
- store,
- provide: {
- groupId: null,
- },
- });
-
- vm = wrapper.vm;
-
- return Vue.nextTick();
- });
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
- it('calls submit if submit button is clicked', () => {
- jest.spyOn(wrapper.vm, 'submit').mockImplementation();
- vm.title = 'Testing Title';
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(wrapper.vm.submit).toHaveBeenCalled();
- });
- });
-
- it('disables submit button if title is empty', () => {
- expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(true);
- });
-
- it('enables submit button if title is not empty', () => {
- wrapper.setData({ title: 'Testing Title' });
-
- return Vue.nextTick().then(() => {
- expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title');
- expect(wrapper.find({ ref: 'submitButton' }).props().disabled).toBe(false);
- });
- });
-
- it('clears title after clicking cancel', () => {
- wrapper.find({ ref: 'cancelButton' }).trigger('click');
-
- return Vue.nextTick().then(() => {
- expect(vm.title).toBe('');
- });
- });
-
- it('does not create new issue if title is empty', () => {
- return submitIssue().then(() => {
- expect(list.newIssue).not.toHaveBeenCalled();
- });
- });
-
- describe('submit success', () => {
- it('creates new issue', () => {
- wrapper.setData({ title: 'create issue' });
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(list.newIssue).toHaveBeenCalled();
- });
- });
-
- it('enables button after submit', () => {
- jest.spyOn(wrapper.vm, 'submit').mockImplementation();
- wrapper.setData({ title: 'create issue' });
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(wrapper.vm.$refs.submitButton.props().disabled).toBe(false);
- });
- });
-
- it('clears title after submit', () => {
- wrapper.setData({ title: 'create issue' });
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(vm.title).toBe('');
- });
- });
-
- it('sets detail issue after submit', () => {
- expect(boardsStore.detail.issue.title).toBe(undefined);
- wrapper.setData({ title: 'create issue' });
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(boardsStore.detail.issue.title).toBe('create issue');
- });
- });
-
- it('sets detail list after submit', () => {
- wrapper.setData({ title: 'create issue' });
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(boardsStore.detail.list.id).toBe(list.id);
- });
- });
-
- it('sets detail weight after submit', () => {
- boardsStore.weightFeatureAvailable = true;
- wrapper.setData({ title: 'create issue' });
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(boardsStore.detail.list.weight).toBe(list.weight);
- });
- });
-
- it('does not set detail weight after submit', () => {
- boardsStore.weightFeatureAvailable = false;
- wrapper.setData({ title: 'create issue' });
-
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(boardsStore.detail.list.weight).toBe(list.weight);
- });
- });
- });
-
- describe('submit error', () => {
- beforeEach(() => {
- newIssueMock = Promise.reject(new Error('My hovercraft is full of eels!'));
- vm.title = 'error';
- });
-
- it('removes issue', () => {
- const lengthBefore = list.issues.length;
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(list.issues.length).toBe(lengthBefore);
- });
- });
-
- it('shows error', () => {
- return Vue.nextTick()
- .then(submitIssue)
- .then(() => {
- expect(vm.error).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
deleted file mode 100644
index 02881333273..00000000000
--- a/spec/frontend/boards/boards_store_spec.js
+++ /dev/null
@@ -1,1013 +0,0 @@
-import AxiosMockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
-import eventHub from '~/boards/eventhub';
-
-import ListIssue from '~/boards/models/issue';
-import List from '~/boards/models/list';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-import { listObj, listObjDuplicate } from './mock_data';
-
-jest.mock('js-cookie');
-
-const createTestIssue = () => ({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
-});
-
-describe('boardsStore', () => {
- const dummyResponse = "without type checking this doesn't matter";
- const boardId = 'dummy-board-id';
- const endpoints = {
- boardsEndpoint: `${TEST_HOST}/boards`,
- listsEndpoint: `${TEST_HOST}/lists`,
- bulkUpdatePath: `${TEST_HOST}/bulk/update`,
- recentBoardsEndpoint: `${TEST_HOST}/recent/boards`,
- };
-
- let axiosMock;
-
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- boardsStore.setEndpoints({
- ...endpoints,
- boardId,
- });
- });
-
- afterEach(() => {
- axiosMock.restore();
- });
-
- const setupDefaultResponses = () => {
- axiosMock
- .onGet(`${endpoints.listsEndpoint}/${listObj.id}/issues?id=${listObj.id}&page=1`)
- .reply(200, { issues: [createTestIssue()] });
- axiosMock.onPost(endpoints.listsEndpoint).reply(200, listObj);
- axiosMock.onPut();
- };
-
- describe('all', () => {
- it('makes a request to fetch lists', () => {
- axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.all()).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500);
-
- return expect(boardsStore.all()).rejects.toThrow();
- });
- });
-
- describe('createList', () => {
- const entityType = 'moorhen';
- const entityId = 'quack';
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({ list: { [entityType]: entityId } }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPost(endpoints.listsEndpoint).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to create a list', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.createList(entityId, entityType))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.createList(entityId, entityType))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('updateList', () => {
- const id = 'David Webb';
- const position = 'unknown';
- const collapsed = false;
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({ list: { position, collapsed } }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to update a list position', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.updateList(id, position, collapsed))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.updateList(id, position, collapsed))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('destroyList', () => {
- const id = '-42';
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock
- .onDelete(`${endpoints.listsEndpoint}/${id}`)
- .replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to delete a list', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.destroyList(id))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalled();
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.destroyList(id))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalled();
- });
- });
- });
-
- describe('saveList', () => {
- let list;
-
- beforeEach(() => {
- list = new List(listObj);
- setupDefaultResponses();
- });
-
- it('makes a request to save a list', () => {
- const expectedResponse = expect.objectContaining({ issues: [createTestIssue()] });
- const expectedListValue = {
- id: listObj.id,
- position: listObj.position,
- type: listObj.list_type,
- label: listObj.label,
- };
- expect(list.id).toBe(listObj.id);
- expect(list.position).toBe(listObj.position);
- expect(list).toMatchObject(expectedListValue);
-
- return expect(boardsStore.saveList(list)).resolves.toEqual(expectedResponse);
- });
- });
-
- describe('getListIssues', () => {
- let list;
-
- beforeEach(() => {
- list = new List(listObj);
- setupDefaultResponses();
- });
-
- it('makes a request to get issues', () => {
- const expectedResponse = expect.objectContaining({ issues: [createTestIssue()] });
- expect(list.issues).toEqual([]);
-
- return expect(boardsStore.getListIssues(list, true)).resolves.toEqual(expectedResponse);
- });
- });
-
- describe('getIssuesForList', () => {
- const id = 'TOO-MUCH';
- const url = `${endpoints.listsEndpoint}/${id}/issues?id=${id}`;
-
- it('makes a request to fetch list issues', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getIssuesForList(id)).resolves.toEqual(expectedResponse);
- });
-
- it('makes a request to fetch list issues with filter', () => {
- const filter = { algal: 'scrubber' };
- axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.getIssuesForList(id)).rejects.toThrow();
- });
- });
-
- describe('moveIssue', () => {
- const urlRoot = 'potato';
- const id = 'over 9000';
- const fromListId = 'left';
- const toListId = 'right';
- const moveBeforeId = 'up';
- const moveAfterId = 'down';
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- }),
- });
-
- let requestSpy;
-
- beforeAll(() => {
- global.gon.relative_url_root = urlRoot;
- });
-
- afterAll(() => {
- delete global.gon.relative_url_root;
- });
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock
- .onPut(`${urlRoot}/-/boards/${boardId}/issues/${id}`)
- .replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to move an issue between lists', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('newIssue', () => {
- const id = 1;
- const issue = { some: 'issue data' };
- const url = `${endpoints.listsEndpoint}/${id}/issues`;
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- issue,
- }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPost(url).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to create a new issue', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.newIssue(id, issue))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.newIssue(id, issue))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('getBacklog', () => {
- const urlRoot = 'deep';
- const url = `${urlRoot}/-/boards/${boardId}/issues.json?not=relevant`;
- const requestParams = {
- not: 'relevant',
- };
-
- beforeAll(() => {
- global.gon.relative_url_root = urlRoot;
- });
-
- afterAll(() => {
- delete global.gon.relative_url_root;
- });
-
- it('makes a request to fetch backlog', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.getBacklog(requestParams)).rejects.toThrow();
- });
- });
-
- describe('bulkUpdate', () => {
- const issueIds = [1, 2, 3];
- const extraData = { moar: 'data' };
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- update: {
- ...extraData,
- issuable_ids: '1,2,3',
- },
- }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to create a list', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.bulkUpdate(issueIds, extraData))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.bulkUpdate(issueIds, extraData))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('getIssueInfo', () => {
- const dummyEndpoint = `${TEST_HOST}/some/where`;
-
- it('makes a request to the given endpoint', () => {
- axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(dummyEndpoint).replyOnce(500);
-
- return expect(boardsStore.getIssueInfo(dummyEndpoint)).rejects.toThrow();
- });
- });
-
- describe('toggleIssueSubscription', () => {
- const dummyEndpoint = `${TEST_HOST}/some/where`;
-
- it('makes a request to the given endpoint', () => {
- axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
- expectedResponse,
- );
- });
-
- it('fails for error response', () => {
- axiosMock.onPost(dummyEndpoint).replyOnce(500);
-
- return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
- });
- });
-
- describe('recentBoards', () => {
- const url = `${endpoints.recentBoardsEndpoint}.json`;
-
- it('makes a request to fetch all boards', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.recentBoards()).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.recentBoards()).rejects.toThrow();
- });
- });
-
- describe('when created', () => {
- beforeEach(() => {
- setupDefaultResponses();
-
- jest.spyOn(boardsStore, 'moveIssue').mockReturnValue(Promise.resolve());
- jest.spyOn(boardsStore, 'moveMultipleIssues').mockReturnValue(Promise.resolve());
-
- boardsStore.create();
- });
-
- it('starts with a blank state', () => {
- expect(boardsStore.state.lists.length).toBe(0);
- });
-
- describe('addList', () => {
- it('sorts by position', () => {
- boardsStore.addList({ position: 2 });
- boardsStore.addList({ position: 1 });
-
- expect(boardsStore.state.lists.map(({ position }) => position)).toEqual([1, 2]);
- });
- });
-
- describe('toggleFilter', () => {
- const dummyFilter = 'x=42';
- let updateTokensSpy;
-
- beforeEach(() => {
- updateTokensSpy = jest.fn();
- eventHub.$once('updateTokens', updateTokensSpy);
-
- // prevent using window.history
- jest.spyOn(boardsStore, 'updateFiltersUrl').mockReturnValue();
- });
-
- it('adds the filter if it is not present', () => {
- boardsStore.filter.path = 'something';
-
- boardsStore.toggleFilter(dummyFilter);
-
- expect(boardsStore.filter.path).toEqual(`something&${dummyFilter}`);
- expect(updateTokensSpy).toHaveBeenCalled();
- expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
- });
-
- it('removes the filter if it is present', () => {
- boardsStore.filter.path = `something&${dummyFilter}`;
-
- boardsStore.toggleFilter(dummyFilter);
-
- expect(boardsStore.filter.path).toEqual('something');
- expect(updateTokensSpy).toHaveBeenCalled();
- expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
- });
- });
-
- describe('lists', () => {
- it('creates new list without persisting to DB', () => {
- expect(boardsStore.state.lists.length).toBe(0);
-
- boardsStore.addList(listObj);
-
- expect(boardsStore.state.lists.length).toBe(1);
- });
-
- it('finds list by ID', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findList('id', listObj.id);
-
- expect(list.id).toBe(listObj.id);
- });
-
- it('finds list by type', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findList('type', 'label');
-
- expect(list).toBeDefined();
- });
-
- it('finds list by label ID', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findListByLabelId(listObj.label.id);
-
- expect(list.id).toBe(listObj.id);
- });
-
- it('gets issue when new list added', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findList('id', listObj.id);
-
- expect(boardsStore.state.lists.length).toBe(1);
-
- return axios.waitForAll().then(() => {
- expect(list.issues.length).toBe(1);
- expect(list.issues[0].id).toBe(1);
- });
- });
-
- it('persists new list', () => {
- boardsStore.new({
- title: 'Test',
- list_type: 'label',
- label: {
- id: 1,
- title: 'Testing',
- color: 'red',
- description: 'testing;',
- },
- });
-
- expect(boardsStore.state.lists.length).toBe(1);
-
- return axios.waitForAll().then(() => {
- const list = boardsStore.findList('id', listObj.id);
-
- expect(list).toEqual(
- expect.objectContaining({
- id: listObj.id,
- position: 0,
- }),
- );
- });
- });
-
- it('removes list from state', () => {
- boardsStore.addList(listObj);
-
- expect(boardsStore.state.lists.length).toBe(1);
-
- boardsStore.removeList(listObj.id);
-
- expect(boardsStore.state.lists.length).toBe(0);
- });
-
- it('moves the position of lists', () => {
- const listOne = boardsStore.addList(listObj);
- boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- boardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]);
-
- expect(listOne.position).toBe(1);
- });
-
- it('moves an issue from one list to another', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
-
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(1);
- });
- });
-
- it('moves an issue from backlog to a list', () => {
- const backlog = boardsStore.addList({
- ...listObj,
- list_type: 'backlog',
- });
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- expect(backlog.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(backlog, listTwo, backlog.findIssue(1));
-
- expect(backlog.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(1);
- });
- });
-
- it('moves issue to top of another list', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- listOne.issues[0].id = 2;
-
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0);
-
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(2);
- expect(listTwo.issues[0].id).toBe(2);
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1);
- });
- });
-
- it('moves issue to bottom of another list', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- listOne.issues[0].id = 2;
-
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1);
-
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(2);
- expect(listTwo.issues[1].id).toBe(2);
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null);
- });
- });
-
- it('moves issue in list', () => {
- const issue = new ListIssue({
- title: 'Testing',
- id: 2,
- iid: 2,
- confidential: false,
- labels: [],
- assignees: [],
- });
- const list = boardsStore.addList(listObj);
-
- return axios.waitForAll().then(() => {
- list.addIssue(issue);
-
- expect(list.issues.length).toBe(2);
-
- boardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]);
-
- expect(list.issues[0].id).toBe(2);
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null);
- });
- });
- });
-
- describe('setListDetail', () => {
- it('sets the list detail', () => {
- boardsStore.detail.list = 'not a list';
-
- const dummyValue = 'new list';
- boardsStore.setListDetail(dummyValue);
-
- expect(boardsStore.detail.list).toEqual(dummyValue);
- });
- });
-
- describe('clearDetailIssue', () => {
- it('resets issue details', () => {
- boardsStore.detail.issue = 'something';
-
- boardsStore.clearDetailIssue();
-
- expect(boardsStore.detail.issue).toEqual({});
- });
- });
-
- describe('setIssueDetail', () => {
- it('sets issue details', () => {
- boardsStore.detail.issue = 'some details';
-
- const dummyValue = 'new details';
- boardsStore.setIssueDetail(dummyValue);
-
- expect(boardsStore.detail.issue).toEqual(dummyValue);
- });
- });
-
- describe('startMoving', () => {
- it('stores list and issue', () => {
- const dummyIssue = 'some issue';
- const dummyList = 'some list';
-
- boardsStore.startMoving(dummyList, dummyIssue);
-
- expect(boardsStore.moving.issue).toEqual(dummyIssue);
- expect(boardsStore.moving.list).toEqual(dummyList);
- });
- });
-
- describe('setTimeTrackingLimitToHours', () => {
- it('sets the timeTracking.LimitToHours option', () => {
- boardsStore.timeTracking.limitToHours = false;
-
- boardsStore.setTimeTrackingLimitToHours('true');
-
- expect(boardsStore.timeTracking.limitToHours).toEqual(true);
- });
- });
-
- describe('setCurrentBoard', () => {
- const dummyBoard = 'hoverboard';
-
- it('sets the current board', () => {
- const { state } = boardsStore;
- state.currentBoard = null;
-
- boardsStore.setCurrentBoard(dummyBoard);
-
- expect(state.currentBoard).toEqual(dummyBoard);
- });
- });
-
- describe('toggleMultiSelect', () => {
- let basicIssueObj;
-
- beforeAll(() => {
- basicIssueObj = { id: 987654 };
- });
-
- afterEach(() => {
- boardsStore.clearMultiSelect();
- });
-
- it('adds issue when not present', () => {
- boardsStore.toggleMultiSelect(basicIssueObj);
-
- const selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
-
- expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
- });
-
- it('removes issue when issue is present', () => {
- boardsStore.toggleMultiSelect(basicIssueObj);
- let selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
-
- expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
-
- boardsStore.toggleMultiSelect(basicIssueObj);
- selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
-
- expect(selectedIds.includes(basicIssueObj.id)).toEqual(false);
- });
- });
-
- describe('clearMultiSelect', () => {
- it('clears all the multi selected issues', () => {
- const issue1 = { id: 12345 };
- const issue2 = { id: 12346 };
-
- boardsStore.toggleMultiSelect(issue1);
- boardsStore.toggleMultiSelect(issue2);
-
- expect(boardsStore.multiSelect.list.length).toEqual(2);
-
- boardsStore.clearMultiSelect();
-
- expect(boardsStore.multiSelect.list.length).toEqual(0);
- });
- });
-
- describe('moveMultipleIssuesToList', () => {
- it('move issues on the new index', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveMultipleIssuesToList({
- listFrom: listOne,
- listTo: listTwo,
- issues: listOne.issues,
- newIndex: 0,
- });
-
- expect(listTwo.issues.length).toBe(1);
- });
- });
- });
-
- describe('moveMultipleIssuesInList', () => {
- it('moves multiple issues in list', () => {
- const issueObj = {
- title: 'Issue #1',
- id: 12345,
- iid: 2,
- confidential: false,
- labels: [],
- assignees: [],
- };
- const issue1 = new ListIssue(issueObj);
- const issue2 = new ListIssue({
- ...issueObj,
- title: 'Issue #2',
- id: 12346,
- });
-
- const list = boardsStore.addList(listObj);
-
- return axios.waitForAll().then(() => {
- list.addIssue(issue1);
- list.addIssue(issue2);
-
- expect(list.issues.length).toBe(3);
- expect(list.issues[0].id).not.toBe(issue2.id);
-
- boardsStore.moveMultipleIssuesInList({
- list,
- issues: [issue1, issue2],
- oldIndicies: [0],
- newIndex: 1,
- idArray: [1, 12345, 12346],
- });
-
- expect(list.issues[0].id).toBe(issue1.id);
-
- expect(boardsStore.moveMultipleIssues).toHaveBeenCalledWith({
- ids: [issue1.id, issue2.id],
- fromListId: null,
- toListId: null,
- moveBeforeId: 1,
- moveAfterId: null,
- });
- });
- });
- });
-
- describe('addListIssue', () => {
- let list;
- const issue1 = new ListIssue({
- title: 'Testing',
- id: 2,
- iid: 2,
- confidential: false,
- labels: [
- {
- color: '#ff0000',
- description: 'testing;',
- id: 5000,
- priority: undefined,
- textColor: 'white',
- title: 'Test',
- },
- ],
- assignees: [],
- });
- const issue2 = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [
- {
- id: 1,
- title: 'test',
- color: 'red',
- description: 'testing',
- },
- ],
- assignees: [
- {
- id: 1,
- name: 'name',
- username: 'username',
- avatar_url: 'http://avatar_url',
- },
- ],
- real_path: 'path/to/issue',
- });
-
- beforeEach(() => {
- list = new List(listObj);
- list.addIssue(issue1);
- setupDefaultResponses();
- });
-
- it('adds issues that are not already on the list', () => {
- expect(list.findIssue(issue2.id)).toBe(undefined);
- expect(list.issues).toEqual([issue1]);
-
- boardsStore.addListIssue(list, issue2);
- expect(list.findIssue(issue2.id)).toBe(issue2);
- expect(list.issues.length).toBe(2);
- expect(list.issues).toEqual([issue1, issue2]);
- });
- });
-
- describe('updateIssue', () => {
- let issue;
- let patchSpy;
-
- beforeEach(() => {
- issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [
- {
- id: 1,
- title: 'test',
- color: 'red',
- description: 'testing',
- },
- ],
- assignees: [
- {
- id: 1,
- name: 'name',
- username: 'username',
- avatar_url: 'http://avatar_url',
- },
- ],
- real_path: 'path/to/issue',
- });
-
- patchSpy = jest.fn().mockReturnValue([200, { labels: [] }]);
- axiosMock.onPatch(`path/to/issue.json`).reply(({ data }) => patchSpy(JSON.parse(data)));
- });
-
- it('passes assignee ids when there are assignees', () => {
- boardsStore.updateIssue(issue);
- return boardsStore.updateIssue(issue).then(() => {
- expect(patchSpy).toHaveBeenCalledWith({
- issue: {
- milestone_id: null,
- assignee_ids: [1],
- label_ids: [1],
- },
- });
- });
- });
-
- it('passes assignee ids of [0] when there are no assignees', () => {
- issue.removeAllAssignees();
-
- return boardsStore.updateIssue(issue).then(() => {
- expect(patchSpy).toHaveBeenCalledWith({
- issue: {
- milestone_id: null,
- assignee_ids: [0],
- label_ids: [1],
- },
- });
- });
- });
- });
- });
-});
diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js
index 61f210f566b..5fae1c4359f 100644
--- a/spec/frontend/boards/components/board_add_new_column_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_spec.js
@@ -48,7 +48,6 @@ describe('Board card layout', () => {
...actions,
},
getters: {
- shouldUseGraphQL: () => true,
getListByLabelId: () => getListByLabelId,
},
state: {
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
new file mode 100644
index 00000000000..dee097bfb08
--- /dev/null
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+import BoardApp from '~/boards/components/board_app.vue';
+
+describe('BoardApp', () => {
+ let wrapper;
+ let store;
+
+ Vue.use(Vuex);
+
+ const createStore = ({ mockGetters = {} } = {}) => {
+ store = new Vuex.Store({
+ state: {},
+ actions: {
+ performSearch: jest.fn(),
+ },
+ getters: {
+ isSidebarOpen: () => true,
+ ...mockGetters,
+ },
+ });
+ };
+
+ const createComponent = ({ provide = { disabled: true } } = {}) => {
+ wrapper = shallowMount(BoardApp, {
+ store,
+ provide: {
+ ...provide,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ store = null;
+ });
+
+ it("should have 'is-compact' class when sidebar is open", () => {
+ createStore();
+ createComponent();
+
+ expect(wrapper.classes()).toContain('is-compact');
+ });
+
+ it("should not have 'is-compact' class when sidebar is closed", () => {
+ createStore({ mockGetters: { isSidebarOpen: () => false } });
+ createComponent();
+
+ expect(wrapper.classes()).not.toContain('is-compact');
+ });
+});
diff --git a/spec/frontend/boards/components/board_card_deprecated_spec.js b/spec/frontend/boards/components/board_card_deprecated_spec.js
deleted file mode 100644
index 266cbc7106d..00000000000
--- a/spec/frontend/boards/components/board_card_deprecated_spec.js
+++ /dev/null
@@ -1,219 +0,0 @@
-/* global List */
-/* global ListAssignee */
-/* global ListLabel */
-
-import { mount } from '@vue/test-utils';
-
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import BoardCardDeprecated from '~/boards/components/board_card_deprecated.vue';
-import issueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
-import eventHub from '~/boards/eventhub';
-import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-
-import sidebarEventHub from '~/sidebar/event_hub';
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/list';
-import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
-
-describe('BoardCard', () => {
- let wrapper;
- let mock;
- let list;
-
- const findIssueCardInner = () => wrapper.find(issueCardInner);
- const findUserAvatarLink = () => wrapper.find(userAvatarLink);
-
- // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
- const mountComponent = (propsData) => {
- wrapper = mount(BoardCardDeprecated, {
- stubs: {
- issueCardInner,
- },
- store,
- propsData: {
- list,
- issue: list.issues[0],
- disabled: false,
- index: 0,
- ...propsData,
- },
- provide: {
- groupId: null,
- rootPath: '/',
- scopedLabelsAvailable: false,
- },
- });
- };
-
- const setupData = async () => {
- list = new List(listObj);
- boardsStore.create();
- boardsStore.detail.issue = {};
- const label1 = new ListLabel({
- id: 3,
- title: 'testing 123',
- color: '#000cff',
- text_color: 'white',
- description: 'test',
- });
- await waitForPromises();
-
- list.issues[0].labels.push(label1);
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- setMockEndpoints();
- return setupData();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- list = null;
- mock.restore();
- });
-
- it('when details issue is empty does not show the element', () => {
- mountComponent();
- expect(wrapper.find('[data-testid="board_card"').classes()).not.toContain('is-active');
- });
-
- it('when detailIssue is equal to card issue shows the element', () => {
- [boardsStore.detail.issue] = list.issues;
- mountComponent();
-
- expect(wrapper.classes()).toContain('is-active');
- });
-
- it('when multiSelect does not contain issue removes multi select class', () => {
- mountComponent();
- expect(wrapper.classes()).not.toContain('multi-select');
- });
-
- it('when multiSelect contain issue add multi select class', () => {
- boardsStore.multiSelect.list = [list.issues[0]];
- mountComponent();
-
- expect(wrapper.classes()).toContain('multi-select');
- });
-
- it('adds user-can-drag class if not disabled', () => {
- mountComponent();
- expect(wrapper.classes()).toContain('user-can-drag');
- });
-
- it('does not add user-can-drag class disabled', () => {
- mountComponent({ disabled: true });
-
- expect(wrapper.classes()).not.toContain('user-can-drag');
- });
-
- it('does not add disabled class', () => {
- mountComponent();
- expect(wrapper.classes()).not.toContain('is-disabled');
- });
-
- it('adds disabled class is disabled is true', () => {
- mountComponent({ disabled: true });
-
- expect(wrapper.classes()).toContain('is-disabled');
- });
-
- describe('mouse events', () => {
- it('does not set detail issue if showDetail is false', () => {
- mountComponent();
- expect(boardsStore.detail.issue).toEqual({});
- });
-
- it('does not set detail issue if link is clicked', () => {
- mountComponent();
- findIssueCardInner().find('a').trigger('mouseup');
-
- expect(boardsStore.detail.issue).toEqual({});
- });
-
- it('does not set detail issue if img is clicked', () => {
- mountComponent({
- issue: {
- ...list.issues[0],
- assignees: [
- new ListAssignee({
- id: 1,
- name: 'testing 123',
- username: 'test',
- avatar: 'test_image',
- }),
- ],
- },
- });
-
- findUserAvatarLink().trigger('mouseup');
-
- expect(boardsStore.detail.issue).toEqual({});
- });
-
- it('does not set detail issue if showDetail is false after mouseup', () => {
- mountComponent();
- wrapper.trigger('mouseup');
-
- expect(boardsStore.detail.issue).toEqual({});
- });
-
- it('sets detail issue to card issue on mouse up', () => {
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
-
- mountComponent();
-
- wrapper.trigger('mousedown');
- wrapper.trigger('mouseup');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false);
- expect(boardsStore.detail.list).toEqual(wrapper.vm.list);
- });
-
- it('resets detail issue to empty if already set', () => {
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- const [issue] = list.issues;
- boardsStore.detail.issue = issue;
- mountComponent();
-
- wrapper.trigger('mousedown');
- wrapper.trigger('mouseup');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false);
- });
- });
-
- describe('sidebarHub events', () => {
- it('closes all sidebars before showing an issue if no issues are opened', () => {
- jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {});
- boardsStore.detail.issue = {};
- mountComponent();
-
- // sets conditional so that event is emitted.
- wrapper.trigger('mousedown');
-
- wrapper.trigger('mouseup');
-
- expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll');
- });
-
- it('it does not closes all sidebars before showing an issue if an issue is opened', () => {
- jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {});
- const [issue] = list.issues;
- boardsStore.detail.issue = issue;
- mountComponent();
-
- wrapper.trigger('mousedown');
-
- expect(sidebarEventHub.$emit).not.toHaveBeenCalledWith('sidebar.closeAll');
- });
- });
-});
diff --git a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js
deleted file mode 100644
index 9853c9f434f..00000000000
--- a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js
+++ /dev/null
@@ -1,158 +0,0 @@
-/* global List */
-/* global ListLabel */
-
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-
-import MockAdapter from 'axios-mock-adapter';
-import Vuex from 'vuex';
-import waitForPromises from 'helpers/wait_for_promises';
-
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/list';
-import BoardCardLayout from '~/boards/components/board_card_layout_deprecated.vue';
-import issueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
-import { ISSUABLE } from '~/boards/constants';
-import boardsVuexStore from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
-
-describe('Board card layout', () => {
- let wrapper;
- let mock;
- let list;
- let store;
-
- const localVue = createLocalVue();
- localVue.use(Vuex);
-
- const createStore = ({ getters = {}, actions = {} } = {}) => {
- store = new Vuex.Store({
- ...boardsVuexStore,
- actions,
- getters,
- });
- };
-
- // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
- const mountComponent = ({ propsData = {}, provide = {} } = {}) => {
- wrapper = shallowMount(BoardCardLayout, {
- localVue,
- stubs: {
- issueCardInner,
- },
- store,
- propsData: {
- list,
- issue: list.issues[0],
- disabled: false,
- index: 0,
- ...propsData,
- },
- provide: {
- groupId: null,
- rootPath: '/',
- scopedLabelsAvailable: false,
- ...provide,
- },
- });
- };
-
- const setupData = () => {
- list = new List(listObj);
- boardsStore.create();
- boardsStore.detail.issue = {};
- const label1 = new ListLabel({
- id: 3,
- title: 'testing 123',
- color: '#000cff',
- text_color: 'white',
- description: 'test',
- });
- return waitForPromises().then(() => {
- list.issues[0].labels.push(label1);
- });
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- setMockEndpoints();
- return setupData();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- list = null;
- mock.restore();
- });
-
- describe('mouse events', () => {
- it('sets showDetail to true on mousedown', async () => {
- createStore();
- mountComponent();
-
- wrapper.trigger('mousedown');
- await wrapper.vm.$nextTick();
-
- expect(wrapper.vm.showDetail).toBe(true);
- });
-
- it('sets showDetail to false on mousemove', async () => {
- createStore();
- mountComponent();
- wrapper.trigger('mousedown');
- await wrapper.vm.$nextTick();
- expect(wrapper.vm.showDetail).toBe(true);
- wrapper.trigger('mousemove');
- await wrapper.vm.$nextTick();
- expect(wrapper.vm.showDetail).toBe(false);
- });
-
- it("calls 'setActiveId' when 'graphqlBoardLists' feature flag is turned on", async () => {
- const setActiveId = jest.fn();
- createStore({
- actions: {
- setActiveId,
- },
- });
- mountComponent({
- provide: {
- glFeatures: { graphqlBoardLists: true },
- },
- });
-
- wrapper.trigger('mouseup');
- await wrapper.vm.$nextTick();
-
- expect(setActiveId).toHaveBeenCalledTimes(1);
- expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
- id: list.issues[0].id,
- sidebarType: ISSUABLE,
- });
- });
-
- it("calls 'setActiveId' when epic swimlanes is active", async () => {
- const setActiveId = jest.fn();
- const isSwimlanesOn = () => true;
- createStore({
- getters: { isSwimlanesOn },
- actions: {
- setActiveId,
- },
- });
- mountComponent();
-
- wrapper.trigger('mouseup');
- await wrapper.vm.$nextTick();
-
- expect(setActiveId).toHaveBeenCalledTimes(1);
- expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
- id: list.issues[0].id,
- sidebarType: ISSUABLE,
- });
- });
- });
-});
diff --git a/spec/frontend/boards/components/board_column_deprecated_spec.js b/spec/frontend/boards/components/board_column_deprecated_spec.js
deleted file mode 100644
index e6d65e48c3f..00000000000
--- a/spec/frontend/boards/components/board_column_deprecated_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
-
-import { TEST_HOST } from 'helpers/test_constants';
-import { listObj } from 'jest/boards/mock_data';
-import Board from '~/boards/components/board_column_deprecated.vue';
-import { ListType } from '~/boards/constants';
-import List from '~/boards/models/list';
-import axios from '~/lib/utils/axios_utils';
-
-describe('Board Column Component', () => {
- let wrapper;
- let axiosMock;
-
- beforeEach(() => {
- window.gon = {};
- axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
- });
-
- afterEach(() => {
- axiosMock.restore();
-
- wrapper.destroy();
-
- localStorage.clear();
- });
-
- const createComponent = ({
- listType = ListType.backlog,
- collapsed = false,
- highlighted = false,
- withLocalStorage = true,
- } = {}) => {
- const boardId = '1';
-
- const listMock = {
- ...listObj,
- list_type: listType,
- highlighted,
- collapsed,
- };
-
- if (listType === ListType.assignee) {
- delete listMock.label;
- listMock.user = {};
- }
-
- // Making List reactive
- const list = Vue.observable(new List(listMock));
-
- if (withLocalStorage) {
- localStorage.setItem(
- `boards.${boardId}.${list.type}.${list.id}.expanded`,
- (!collapsed).toString(),
- );
- }
-
- wrapper = shallowMount(Board, {
- propsData: {
- boardId,
- disabled: false,
- list,
- },
- provide: {
- boardId,
- },
- });
- };
-
- const isExpandable = () => wrapper.classes('is-expandable');
- const isCollapsed = () => wrapper.classes('is-collapsed');
-
- describe('Given different list types', () => {
- it('is expandable when List Type is `backlog`', () => {
- createComponent({ listType: ListType.backlog });
-
- expect(isExpandable()).toBe(true);
- });
- });
-
- describe('expanded / collapsed column', () => {
- it('has class is-collapsed when list is collapsed', () => {
- createComponent({ collapsed: false });
-
- expect(wrapper.vm.list.isExpanded).toBe(true);
- });
-
- it('does not have class is-collapsed when list is expanded', () => {
- createComponent({ collapsed: true });
-
- expect(isCollapsed()).toBe(true);
- });
- });
-
- describe('highlighting', () => {
- it('scrolls to column when highlighted', async () => {
- createComponent({ highlighted: true });
-
- await nextTick();
-
- expect(wrapper.element.scrollIntoView).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 5a799b6388e..f535679b8a0 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -5,9 +5,10 @@ import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
-import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.vue';
+import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
-import { mockLists, mockListsWithModel } from '../mock_data';
+import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
+import { mockLists } from '../mock_data';
Vue.use(Vuex);
@@ -23,6 +24,7 @@ describe('BoardContent', () => {
isShowingEpicsSwimlanes: false,
boardLists: mockLists,
error: undefined,
+ issuableType: 'issue',
};
const createStore = (state = defaultState) => {
@@ -33,25 +35,19 @@ describe('BoardContent', () => {
});
};
- const createComponent = ({
- state,
- props = {},
- graphqlBoardListsEnabled = false,
- canAdminList = true,
- } = {}) => {
+ const createComponent = ({ state, props = {}, canAdminList = true } = {}) => {
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
propsData: {
- lists: mockListsWithModel,
+ lists: mockLists,
disabled: false,
...props,
},
provide: {
canAdminList,
- glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled },
},
store,
});
@@ -61,53 +57,48 @@ describe('BoardContent', () => {
wrapper.destroy();
});
- it('renders a BoardColumnDeprecated component per list', () => {
- createComponent();
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(wrapper.findAllComponents(BoardColumnDeprecated)).toHaveLength(
- mockListsWithModel.length,
- );
- });
+ it('renders a BoardColumn component per list', () => {
+ expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
+ });
- it('does not display EpicsSwimlanes component', () => {
- createComponent();
+ it('renders BoardContentSidebar', () => {
+ expect(wrapper.find(BoardContentSidebar).exists()).toBe(true);
+ });
- expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
- expect(wrapper.find(GlAlert).exists()).toBe(false);
+ it('does not display EpicsSwimlanes component', () => {
+ expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
});
- describe('graphqlBoardLists feature flag enabled', () => {
+ describe('when issuableType is not issue', () => {
beforeEach(() => {
- createComponent({ graphqlBoardListsEnabled: true });
- gon.features = {
- graphqlBoardLists: true,
- };
+ createComponent({ state: { issuableType: 'foo' } });
});
- describe('can admin list', () => {
- beforeEach(() => {
- createComponent({ graphqlBoardListsEnabled: true, canAdminList: true });
- });
-
- it('renders draggable component', () => {
- expect(wrapper.find(Draggable).exists()).toBe(true);
- });
+ it('does not render BoardContentSidebar', () => {
+ expect(wrapper.find(BoardContentSidebar).exists()).toBe(false);
});
+ });
- describe('can not admin list', () => {
- beforeEach(() => {
- createComponent({ graphqlBoardListsEnabled: true, canAdminList: false });
- });
+ describe('can admin list', () => {
+ beforeEach(() => {
+ createComponent({ canAdminList: true });
+ });
- it('does not render draggable component', () => {
- expect(wrapper.find(Draggable).exists()).toBe(false);
- });
+ it('renders draggable component', () => {
+ expect(wrapper.find(Draggable).exists()).toBe(true);
});
});
- describe('graphqlBoardLists feature flag disabled', () => {
+ describe('can not admin list', () => {
beforeEach(() => {
- createComponent({ graphqlBoardListsEnabled: false });
+ createComponent({ canAdminList: false });
});
it('does not render draggable component', () => {
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 50f86e92adb..dc93890f27a 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
-import { createStore } from '~/boards/stores';
import * as urlUtility from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@@ -44,6 +43,12 @@ describe('BoardFilteredSearch', () => {
];
const createComponent = ({ initialFilterParams = {} } = {}) => {
+ store = new Vuex.Store({
+ actions: {
+ performSearch: jest.fn(),
+ },
+ });
+
wrapper = shallowMount(BoardFilteredSearch, {
provide: { initialFilterParams, fullPath: '' },
store,
@@ -55,22 +60,15 @@ describe('BoardFilteredSearch', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBarRoot);
- beforeEach(() => {
- // this needed for actions call for performSearch
- window.gon = { features: {} };
- });
-
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
- store = createStore();
+ createComponent();
jest.spyOn(store, 'dispatch');
-
- createComponent();
});
it('renders FilteredSearch', () => {
@@ -103,8 +101,6 @@ describe('BoardFilteredSearch', () => {
describe('when searching', () => {
beforeEach(() => {
- store = createStore();
-
createComponent();
jest.spyOn(wrapper.vm, 'performSearch').mockImplementation();
@@ -133,11 +129,9 @@ describe('BoardFilteredSearch', () => {
describe('when url params are already set', () => {
beforeEach(() => {
- store = createStore();
+ createComponent({ initialFilterParams: { authorUsername: 'root', labelName: ['label'] } });
jest.spyOn(store, 'dispatch');
-
- createComponent({ initialFilterParams: { authorUsername: 'root', labelName: ['label'] } });
});
it('passes the correct props to FilterSearchBar', () => {
diff --git a/spec/frontend/boards/components/board_list_header_deprecated_spec.js b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
deleted file mode 100644
index db79e67fe78..00000000000
--- a/spec/frontend/boards/components/board_list_header_deprecated_spec.js
+++ /dev/null
@@ -1,174 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
-
-import { TEST_HOST } from 'helpers/test_constants';
-import { listObj } from 'jest/boards/mock_data';
-import BoardListHeader from '~/boards/components/board_list_header_deprecated.vue';
-import { ListType } from '~/boards/constants';
-import List from '~/boards/models/list';
-import axios from '~/lib/utils/axios_utils';
-
-describe('Board List Header Component', () => {
- let wrapper;
- let axiosMock;
-
- beforeEach(() => {
- window.gon = {};
- axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(`${TEST_HOST}/lists/1/issues`).reply(200, { issues: [] });
- });
-
- afterEach(() => {
- axiosMock.restore();
-
- wrapper.destroy();
-
- localStorage.clear();
- });
-
- const createComponent = ({
- listType = ListType.backlog,
- collapsed = false,
- withLocalStorage = true,
- currentUserId = 1,
- } = {}) => {
- const boardId = '1';
-
- const listMock = {
- ...listObj,
- list_type: listType,
- collapsed,
- };
-
- if (listType === ListType.assignee) {
- delete listMock.label;
- listMock.user = {};
- }
-
- // Making List reactive
- const list = Vue.observable(new List(listMock));
-
- if (withLocalStorage) {
- localStorage.setItem(
- `boards.${boardId}.${list.type}.${list.id}.expanded`,
- (!collapsed).toString(),
- );
- }
-
- wrapper = shallowMount(BoardListHeader, {
- propsData: {
- disabled: false,
- list,
- },
- provide: {
- boardId,
- currentUserId,
- },
- });
- };
-
- const isCollapsed = () => !wrapper.props().list.isExpanded;
- const isExpanded = () => wrapper.vm.list.isExpanded;
-
- const findAddIssueButton = () => wrapper.find({ ref: 'newIssueBtn' });
- const findCaret = () => wrapper.find('.board-title-caret');
-
- describe('Add issue button', () => {
- const hasNoAddButton = [ListType.closed];
- const hasAddButton = [
- ListType.backlog,
- ListType.label,
- ListType.milestone,
- ListType.iteration,
- ListType.assignee,
- ];
-
- it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
- createComponent({ listType });
-
- expect(findAddIssueButton().exists()).toBe(false);
- });
-
- it.each(hasAddButton)('does render when List Type is `%s`', (listType) => {
- createComponent({ listType });
-
- expect(findAddIssueButton().exists()).toBe(true);
- });
-
- it('has a test for each list type', () => {
- Object.values(ListType).forEach((value) => {
- expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
- });
- });
-
- it('does not render when logged out', () => {
- createComponent({
- currentUserId: null,
- });
-
- expect(findAddIssueButton().exists()).toBe(false);
- });
- });
-
- describe('expanding / collapsing the column', () => {
- it('does not collapse when clicking the header', () => {
- createComponent();
-
- expect(isCollapsed()).toBe(false);
- wrapper.find('[data-testid="board-list-header"]').trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(isCollapsed()).toBe(false);
- });
- });
-
- it('collapses expanded Column when clicking the collapse icon', () => {
- createComponent();
-
- expect(isExpanded()).toBe(true);
- findCaret().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(isCollapsed()).toBe(true);
- });
- });
-
- it('expands collapsed Column when clicking the expand icon', () => {
- createComponent({ collapsed: true });
-
- expect(isCollapsed()).toBe(true);
- findCaret().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(isCollapsed()).toBe(false);
- });
- });
-
- it("when logged in it calls list update and doesn't set localStorage", () => {
- jest.spyOn(List.prototype, 'update');
-
- createComponent({ withLocalStorage: false });
-
- findCaret().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.list.update).toHaveBeenCalledTimes(1);
- expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(null);
- });
- });
-
- it("when logged out it doesn't call list update and sets localStorage", () => {
- jest.spyOn(List.prototype, 'update');
-
- createComponent({ currentUserId: null });
-
- findCaret().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.list.update).not.toHaveBeenCalled();
- expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.expanded`)).toBe(String(isExpanded()));
- });
- });
- });
-});
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 20a08be6c19..46dd109ffb1 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -1,38 +1,55 @@
-import '~/boards/models/list';
import { GlDrawer, GlLabel } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
+import Vue from 'vue';
import Vuex from 'vuex';
+import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import { inactiveId, LIST } from '~/boards/constants';
-import { createStore } from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
+import actions from '~/boards/stores/actions';
+import getters from '~/boards/stores/getters';
+import mutations from '~/boards/stores/mutations';
import sidebarEventHub from '~/sidebar/event_hub';
+import { mockLabelList } from '../mock_data';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('BoardSettingsSidebar', () => {
let wrapper;
- let mock;
- let store;
- const labelTitle = 'test';
- const labelColor = '#FFFF';
- const listId = 1;
+ const labelTitle = mockLabelList.label.title;
+ const labelColor = mockLabelList.label.color;
+ const listId = mockLabelList.id;
const findRemoveButton = () => wrapper.findByTestId('remove-list');
- const createComponent = ({ canAdminList = false } = {}) => {
+ const createComponent = ({
+ canAdminList = false,
+ list = {},
+ sidebarType = LIST,
+ activeId = inactiveId,
+ } = {}) => {
+ const boardLists = {
+ [listId]: list,
+ };
+ const store = new Vuex.Store({
+ state: { sidebarType, activeId, boardLists },
+ getters,
+ mutations,
+ actions,
+ });
+
wrapper = extendedWrapper(
shallowMount(BoardSettingsSidebar, {
store,
- localVue,
provide: {
canAdminList,
+ scopedLabelsAvailable: false,
+ },
+ stubs: {
+ GlDrawer: stubComponent(GlDrawer, {
+ template: '<div><slot name="header"></slot><slot></slot></div>',
+ }),
},
}),
);
@@ -40,16 +57,10 @@ describe('BoardSettingsSidebar', () => {
const findLabel = () => wrapper.find(GlLabel);
const findDrawer = () => wrapper.find(GlDrawer);
- beforeEach(() => {
- store = createStore();
- store.state.activeId = inactiveId;
- store.state.sidebarType = LIST;
- boardsStore.create();
- });
-
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
+ wrapper = null;
});
it('finds a MountingPortal component', () => {
@@ -100,86 +111,40 @@ describe('BoardSettingsSidebar', () => {
});
describe('when activeId is greater than zero', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- boardsStore.addList({
- id: listId,
- label: { title: labelTitle, color: labelColor },
- list_type: 'label',
- });
- store.state.activeId = 1;
- store.state.sidebarType = LIST;
- });
-
- afterEach(() => {
- boardsStore.removeList(listId);
- });
-
- it('renders GlDrawer with open false', () => {
- createComponent();
+ it('renders GlDrawer with open true', () => {
+ createComponent({ list: mockLabelList, activeId: listId });
expect(findDrawer().props('open')).toBe(true);
});
});
- describe('when activeId is in boardsStore', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- boardsStore.addList({
- id: listId,
- label: { title: labelTitle, color: labelColor },
- list_type: 'label',
- });
-
- store.state.activeId = listId;
- store.state.sidebarType = LIST;
-
- createComponent();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
+ describe('when activeId is in state', () => {
it('renders label title', () => {
+ createComponent({ list: mockLabelList, activeId: listId });
+
expect(findLabel().props('title')).toBe(labelTitle);
});
it('renders label background color', () => {
+ createComponent({ list: mockLabelList, activeId: listId });
+
expect(findLabel().props('backgroundColor')).toBe(labelColor);
});
});
- describe('when activeId is not in boardsStore', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
-
- store.state.activeId = inactiveId;
-
- createComponent();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
+ describe('when activeId is not in state', () => {
it('does not render GlLabel', () => {
+ createComponent({ list: mockLabelList });
+
expect(findLabel().exists()).toBe(false);
});
});
});
describe('when sidebarType is not List', () => {
- beforeEach(() => {
- store.state.sidebarType = '';
- createComponent();
- });
-
it('does not render GlDrawer', () => {
+ createComponent({ sidebarType: '' });
+
expect(findDrawer().exists()).toBe(false);
});
});
@@ -191,20 +156,9 @@ describe('BoardSettingsSidebar', () => {
});
describe('when user can admin the boards list', () => {
- beforeEach(() => {
- store.state.activeId = listId;
- store.state.sidebarType = LIST;
-
- boardsStore.addList({
- id: listId,
- label: { title: labelTitle, color: labelColor },
- list_type: 'label',
- });
-
- createComponent({ canAdminList: true });
- });
-
it('renders "Remove list" button', () => {
+ createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
+
expect(findRemoveButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/boards/components/boards_selector_deprecated_spec.js b/spec/frontend/boards/components/boards_selector_deprecated_spec.js
deleted file mode 100644
index cc078861d75..00000000000
--- a/spec/frontend/boards/components/boards_selector_deprecated_spec.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'spec/test_constants';
-import BoardsSelector from '~/boards/components/boards_selector_deprecated.vue';
-import boardsStore from '~/boards/stores/boards_store';
-
-const throttleDuration = 1;
-
-function boardGenerator(n) {
- return new Array(n).fill().map((board, index) => {
- const id = `${index}`;
- const name = `board${id}`;
-
- return {
- id,
- name,
- };
- });
-}
-
-describe('BoardsSelector', () => {
- let wrapper;
- let allBoardsResponse;
- let recentBoardsResponse;
- const boards = boardGenerator(20);
- const recentBoards = boardGenerator(5);
-
- const fillSearchBox = (filterTerm) => {
- const searchBox = wrapper.find({ ref: 'searchBox' });
- const searchBoxInput = searchBox.find('input');
- searchBoxInput.setValue(filterTerm);
- searchBoxInput.trigger('input');
- };
-
- const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
- const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader);
- const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findDropdown = () => wrapper.find(GlDropdown);
-
- beforeEach(() => {
- const $apollo = {
- queries: {
- boards: {
- loading: false,
- },
- },
- };
-
- boardsStore.setEndpoints({
- boardsEndpoint: '',
- recentBoardsEndpoint: '',
- listsEndpoint: '',
- bulkUpdatePath: '',
- boardId: '',
- });
-
- allBoardsResponse = Promise.resolve({
- data: {
- group: {
- boards: {
- edges: boards.map((board) => ({ node: board })),
- },
- },
- },
- });
- recentBoardsResponse = Promise.resolve({
- data: recentBoards,
- });
-
- boardsStore.allBoards = jest.fn(() => allBoardsResponse);
- boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
-
- wrapper = mount(BoardsSelector, {
- propsData: {
- throttleDuration,
- currentBoard: {
- id: 1,
- name: 'Development',
- milestone_id: null,
- weight: null,
- assignee_id: null,
- labels: [],
- },
- boardBaseUrl: `${TEST_HOST}/board/base/url`,
- hasMissingBoards: false,
- canAdminBoard: true,
- multipleIssueBoardsAvailable: true,
- labelsPath: `${TEST_HOST}/labels/path`,
- labelsWebUrl: `${TEST_HOST}/labels`,
- projectId: 42,
- groupId: 19,
- scopedIssueBoardFeatureEnabled: true,
- weights: [],
- },
- mocks: { $apollo },
- attachTo: document.body,
- });
-
- wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
- wrapper.setData({
- [options.loadingKey]: true,
- });
- });
-
- // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- findDropdown().vm.$emit('show');
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('loading', () => {
- // we are testing loading state, so don't resolve responses until after the tests
- afterEach(() => {
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
- });
-
- it('shows loading spinner', () => {
- expect(getDropdownHeaders()).toHaveLength(0);
- expect(getDropdownItems()).toHaveLength(0);
- expect(getLoadingIcon().exists()).toBe(true);
- });
- });
-
- describe('loaded', () => {
- beforeEach(async () => {
- await wrapper.setData({
- loadingBoards: false,
- });
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
- });
-
- it('hides loading spinner', () => {
- expect(getLoadingIcon().exists()).toBe(false);
- });
-
- describe('filtering', () => {
- beforeEach(() => {
- wrapper.setData({
- boards,
- });
-
- return nextTick();
- });
-
- it('shows all boards without filtering', () => {
- expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
- });
-
- it('shows only matching boards when filtering', () => {
- const filterTerm = 'board1';
- const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
-
- fillSearchBox(filterTerm);
-
- return nextTick().then(() => {
- expect(getDropdownItems()).toHaveLength(expectedCount);
- });
- });
-
- it('shows message if there are no matching boards', () => {
- fillSearchBox('does not exist');
-
- return nextTick().then(() => {
- expect(getDropdownItems()).toHaveLength(0);
- expect(wrapper.text().includes('No matching boards found')).toBe(true);
- });
- });
- });
-
- describe('recent boards section', () => {
- it('shows only when boards are greater than 10', () => {
- wrapper.setData({
- boards,
- });
-
- return nextTick().then(() => {
- expect(getDropdownHeaders()).toHaveLength(2);
- });
- });
-
- it('does not show when boards are less than 10', () => {
- wrapper.setData({
- boards: boards.slice(0, 5),
- });
-
- return nextTick().then(() => {
- expect(getDropdownHeaders()).toHaveLength(0);
- });
- });
-
- it('does not show when recentBoards api returns empty array', () => {
- wrapper.setData({
- recentBoards: [],
- });
-
- return nextTick().then(() => {
- expect(getDropdownHeaders()).toHaveLength(0);
- });
- });
-
- it('does not show when search is active', () => {
- fillSearchBox('Random string');
-
- return nextTick().then(() => {
- expect(getDropdownHeaders()).toHaveLength(0);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js b/spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js
deleted file mode 100644
index fafebaf3a4e..00000000000
--- a/spec/frontend/boards/components/issue_time_estimate_deprecated_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import IssueTimeEstimate from '~/boards/components/issue_time_estimate_deprecated.vue';
-import boardsStore from '~/boards/stores/boards_store';
-
-describe('Issue Time Estimate component', () => {
- let wrapper;
-
- beforeEach(() => {
- boardsStore.create();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when limitToHours is false', () => {
- beforeEach(() => {
- boardsStore.timeTracking.limitToHours = false;
- wrapper = shallowMount(IssueTimeEstimate, {
- propsData: {
- estimate: 374460,
- },
- });
- });
-
- it('renders the correct time estimate', () => {
- expect(wrapper.find('time').text().trim()).toEqual('2w 3d 1m');
- });
-
- it('renders expanded time estimate in tooltip', () => {
- expect(wrapper.find('.js-issue-time-estimate').text()).toContain('2 weeks 3 days 1 minute');
- });
-
- it('prevents tooltip xss', (done) => {
- const alertSpy = jest.spyOn(window, 'alert');
- wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
- wrapper.vm.$nextTick(() => {
- expect(alertSpy).not.toHaveBeenCalled();
- expect(wrapper.find('time').text().trim()).toEqual('0m');
- expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m');
- done();
- });
- });
- });
-
- describe('when limitToHours is true', () => {
- beforeEach(() => {
- boardsStore.timeTracking.limitToHours = true;
- wrapper = shallowMount(IssueTimeEstimate, {
- propsData: {
- estimate: 374460,
- },
- });
- });
-
- it('renders the correct time estimate', () => {
- expect(wrapper.find('time').text().trim()).toEqual('104h 1m');
- });
-
- it('renders expanded time estimate in tooltip', () => {
- expect(wrapper.find('.js-issue-time-estimate').text()).toContain('104 hours 1 minute');
- });
- });
-});
diff --git a/spec/frontend/boards/issue_card_deprecated_spec.js b/spec/frontend/boards/issue_card_deprecated_spec.js
deleted file mode 100644
index 909be275030..00000000000
--- a/spec/frontend/boards/issue_card_deprecated_spec.js
+++ /dev/null
@@ -1,332 +0,0 @@
-/* global ListAssignee, ListLabel, ListIssue */
-import { GlLabel } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { range } from 'lodash';
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
-import store from '~/boards/stores';
-import { listObj } from './mock_data';
-
-describe('Issue card component', () => {
- const user = new ListAssignee({
- id: 1,
- name: 'testing 123',
- username: 'test',
- avatar: 'test_image',
- });
-
- const label1 = new ListLabel({
- id: 3,
- title: 'testing 123',
- color: '#000CFF',
- text_color: 'white',
- description: 'test',
- });
-
- let wrapper;
- let issue;
- let list;
-
- beforeEach(() => {
- list = { ...listObj, type: 'label' };
- issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [list.label],
- assignees: [],
- reference_path: '#1',
- real_path: '/test/1',
- weight: 1,
- });
- wrapper = mount(IssueCardInner, {
- propsData: {
- list,
- issue,
- },
- store,
- stubs: {
- GlLabel: true,
- },
- provide: {
- groupId: null,
- rootPath: '/',
- },
- });
- });
-
- it('renders issue title', () => {
- expect(wrapper.find('.board-card-title').text()).toContain(issue.title);
- });
-
- it('includes issue base in link', () => {
- expect(wrapper.find('.board-card-title a').attributes('href')).toContain('/test');
- });
-
- it('includes issue title on link', () => {
- expect(wrapper.find('.board-card-title a').attributes('title')).toBe(issue.title);
- });
-
- it('does not render confidential icon', () => {
- expect(wrapper.find('.confidential-icon').exists()).toBe(false);
- });
-
- it('does not render blocked icon', () => {
- expect(wrapper.find('.issue-blocked-icon').exists()).toBe(false);
- });
-
- it('renders confidential icon', (done) => {
- wrapper.setProps({
- issue: {
- ...wrapper.props('issue'),
- confidential: true,
- },
- });
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.confidential-icon').exists()).toBe(true);
- done();
- });
- });
-
- it('renders issue ID with #', () => {
- expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.id}`);
- });
-
- describe('assignee', () => {
- it('does not render assignee', () => {
- expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false);
- });
-
- describe('exists', () => {
- beforeEach((done) => {
- wrapper.setProps({
- issue: {
- ...wrapper.props('issue'),
- assignees: [user],
- updateData(newData) {
- Object.assign(this, newData);
- },
- },
- });
-
- wrapper.vm.$nextTick(done);
- });
-
- it('renders assignee', () => {
- expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(true);
- });
-
- it('sets title', () => {
- expect(wrapper.find('.js-assignee-tooltip').text()).toContain(`${user.name}`);
- });
-
- it('sets users path', () => {
- expect(wrapper.find('.board-card-assignee a').attributes('href')).toBe('/test');
- });
-
- it('renders avatar', () => {
- expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
- });
-
- it('renders the avatar using avatar_url property', (done) => {
- wrapper.props('issue').updateData({
- ...wrapper.props('issue'),
- assignees: [
- {
- id: '1',
- name: 'test',
- state: 'active',
- username: 'test_name',
- avatar_url: 'test_image_from_avatar_url',
- },
- ],
- });
-
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
- 'test_image_from_avatar_url?width=24',
- );
- done();
- });
- });
- });
-
- describe('assignee default avatar', () => {
- beforeEach((done) => {
- global.gon.default_avatar_url = 'default_avatar';
-
- wrapper.setProps({
- issue: {
- ...wrapper.props('issue'),
- assignees: [
- new ListAssignee({
- id: 1,
- name: 'testing 123',
- username: 'test',
- }),
- ],
- },
- });
-
- wrapper.vm.$nextTick(done);
- });
-
- afterEach(() => {
- global.gon.default_avatar_url = null;
- });
-
- it('displays defaults avatar if users avatar is null', () => {
- expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
- expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
- 'default_avatar?width=24',
- );
- });
- });
- });
-
- describe('multiple assignees', () => {
- beforeEach((done) => {
- wrapper.setProps({
- issue: {
- ...wrapper.props('issue'),
- assignees: [
- new ListAssignee({
- id: 2,
- name: 'user2',
- username: 'user2',
- avatar: 'test_image',
- }),
- new ListAssignee({
- id: 3,
- name: 'user3',
- username: 'user3',
- avatar: 'test_image',
- }),
- new ListAssignee({
- id: 4,
- name: 'user4',
- username: 'user4',
- avatar: 'test_image',
- }),
- ],
- },
- });
-
- wrapper.vm.$nextTick(done);
- });
-
- it('renders all three assignees', () => {
- expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(3);
- });
-
- describe('more than three assignees', () => {
- beforeEach((done) => {
- const { assignees } = wrapper.props('issue');
- assignees.push(
- new ListAssignee({
- id: 5,
- name: 'user5',
- username: 'user5',
- avatar: 'test_image',
- }),
- );
-
- wrapper.setProps({
- issue: {
- ...wrapper.props('issue'),
- assignees,
- },
- });
- wrapper.vm.$nextTick(done);
- });
-
- it('renders more avatar counter', () => {
- expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('+2');
- });
-
- it('renders two assignees', () => {
- expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(2);
- });
-
- it('renders 99+ avatar counter', (done) => {
- const assignees = [
- ...wrapper.props('issue').assignees,
- ...range(5, 103).map(
- (i) =>
- new ListAssignee({
- id: i,
- name: 'name',
- username: 'username',
- avatar: 'test_image',
- }),
- ),
- ];
- wrapper.setProps({
- issue: {
- ...wrapper.props('issue'),
- assignees,
- },
- });
-
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.board-card-assignee .avatar-counter').text().trim()).toEqual('99+');
- done();
- });
- });
- });
- });
-
- describe('labels', () => {
- beforeEach((done) => {
- issue.addLabel(label1);
- wrapper.setProps({ issue: { ...issue } });
-
- wrapper.vm.$nextTick(done);
- });
-
- it('does not render list label but renders all other labels', () => {
- expect(wrapper.findAll(GlLabel).length).toBe(1);
- const label = wrapper.find(GlLabel);
- expect(label.props('title')).toEqual(label1.title);
- expect(label.props('description')).toEqual(label1.description);
- expect(label.props('backgroundColor')).toEqual(label1.color);
- });
-
- it('does not render label if label does not have an ID', (done) => {
- issue.addLabel(
- new ListLabel({
- title: 'closed',
- }),
- );
- wrapper.setProps({ issue: { ...issue } });
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.findAll(GlLabel).length).toBe(1);
- expect(wrapper.text()).not.toContain('closed');
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('blocked', () => {
- beforeEach((done) => {
- wrapper.setProps({
- issue: {
- ...wrapper.props('issue'),
- blocked: true,
- },
- });
- wrapper.vm.$nextTick(done);
- });
-
- it('renders blocked icon if issue is blocked', () => {
- expect(wrapper.find('.issue-blocked-icon').exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js
deleted file mode 100644
index 1f354fb04db..00000000000
--- a/spec/frontend/boards/issue_spec.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/* global ListIssue */
-
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import boardsStore from '~/boards/stores/boards_store';
-import { setMockEndpoints, mockIssue } from './mock_data';
-
-describe('Issue model', () => {
- let issue;
-
- beforeEach(() => {
- setMockEndpoints();
- boardsStore.create();
-
- issue = new ListIssue(mockIssue);
- });
-
- it('has label', () => {
- expect(issue.labels.length).toBe(1);
- });
-
- it('add new label', () => {
- issue.addLabel({
- id: 2,
- title: 'bug',
- color: 'blue',
- description: 'bugs!',
- });
-
- expect(issue.labels.length).toBe(2);
- });
-
- it('does not add label if label id exists', () => {
- issue.addLabel({
- id: 1,
- title: 'test 2',
- color: 'blue',
- description: 'testing',
- });
-
- expect(issue.labels.length).toBe(1);
- expect(issue.labels[0].color).toBe('#F0AD4E');
- });
-
- it('adds other label with same title', () => {
- issue.addLabel({
- id: 2,
- title: 'test',
- color: 'blue',
- description: 'other test',
- });
-
- expect(issue.labels.length).toBe(2);
- });
-
- it('finds label', () => {
- const label = issue.findLabel(issue.labels[0]);
-
- expect(label).toBeDefined();
- });
-
- it('removes label', () => {
- const label = issue.findLabel(issue.labels[0]);
- issue.removeLabel(label);
-
- expect(issue.labels.length).toBe(0);
- });
-
- it('removes multiple labels', () => {
- issue.addLabel({
- id: 2,
- title: 'bug',
- color: 'blue',
- description: 'bugs!',
- });
-
- expect(issue.labels.length).toBe(2);
-
- issue.removeLabels([issue.labels[0], issue.labels[1]]);
-
- expect(issue.labels.length).toBe(0);
- });
-
- it('adds assignee', () => {
- issue.addAssignee({
- id: 2,
- name: 'Bruce Wayne',
- username: 'batman',
- avatar_url: 'http://batman',
- });
-
- expect(issue.assignees.length).toBe(2);
- });
-
- it('finds assignee', () => {
- const assignee = issue.findAssignee(issue.assignees[0]);
-
- expect(assignee).toBeDefined();
- });
-
- it('removes assignee', () => {
- const assignee = issue.findAssignee(issue.assignees[0]);
- issue.removeAssignee(assignee);
-
- expect(issue.assignees.length).toBe(0);
- });
-
- it('removes all assignees', () => {
- issue.removeAllAssignees();
-
- expect(issue.assignees.length).toBe(0);
- });
-
- it('sets position to infinity if no position is stored', () => {
- expect(issue.position).toBe(Infinity);
- });
-
- it('sets position', () => {
- const relativePositionIssue = new ListIssue({
- title: 'Testing',
- iid: 1,
- confidential: false,
- relative_position: 1,
- labels: [],
- assignees: [],
- });
-
- expect(relativePositionIssue.position).toBe(1);
- });
-
- it('updates data', () => {
- issue.updateData({ subscribed: true });
-
- expect(issue.subscribed).toBe(true);
- });
-
- it('sets fetching state', () => {
- expect(issue.isFetching.subscriptions).toBe(true);
-
- issue.setFetchingState('subscriptions', false);
-
- expect(issue.isFetching.subscriptions).toBe(false);
- });
-
- it('sets loading state', () => {
- issue.setLoadingState('foo', true);
-
- expect(issue.isLoading.foo).toBe(true);
- });
-
- describe('update', () => {
- it('passes update to boardsStore', () => {
- jest.spyOn(boardsStore, 'updateIssue').mockImplementation();
-
- issue.update();
-
- expect(boardsStore.updateIssue).toHaveBeenCalledWith(issue);
- });
- });
-});
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
deleted file mode 100644
index 4d6a82bdff0..00000000000
--- a/spec/frontend/boards/list_spec.js
+++ /dev/null
@@ -1,230 +0,0 @@
-/* global List */
-/* global ListAssignee */
-/* global ListIssue */
-/* global ListLabel */
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import { ListType } from '~/boards/constants';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
-
-describe('List model', () => {
- let list;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- boardsStore.create();
- boardsStore.setEndpoints({
- listsEndpoint: '/test/-/boards/1/lists',
- });
-
- list = new List(listObj);
- return waitForPromises();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('list type', () => {
- const notExpandableList = ['blank'];
-
- const table = Object.keys(ListType).map((k) => {
- const value = ListType[k];
- return [value, !notExpandableList.includes(value)];
- });
- it.each(table)(`when list_type is %s boards isExpandable is %p`, (type, result) => {
- expect(new List({ id: 1, list_type: type }).isExpandable).toBe(result);
- });
- });
-
- it('gets issues when created', () => {
- expect(list.issues.length).toBe(1);
- });
-
- it('saves list and returns ID', () => {
- list = new List({
- title: 'test',
- label: {
- id: 1,
- title: 'test',
- color: '#ff0000',
- text_color: 'white',
- },
- });
- return list.save().then(() => {
- expect(list.id).toBe(listObj.id);
- expect(list.type).toBe('label');
- expect(list.position).toBe(0);
- expect(list.label).toEqual(listObj.label);
- });
- });
-
- it('destroys the list', () => {
- boardsStore.addList(listObj);
- list = boardsStore.findList('id', listObj.id);
-
- expect(boardsStore.state.lists.length).toBe(1);
- list.destroy();
-
- return waitForPromises().then(() => {
- expect(boardsStore.state.lists.length).toBe(0);
- });
- });
-
- it('gets issue from list', () => {
- const issue = list.findIssue(1);
-
- expect(issue).toBeDefined();
- });
-
- it('removes issue', () => {
- const issue = list.findIssue(1);
-
- expect(list.issues.length).toBe(1);
- list.removeIssue(issue);
-
- expect(list.issues.length).toBe(0);
- });
-
- it('sends service request to update issue label', () => {
- const listDup = new List(listObjDuplicate);
- const issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [list.label, listDup.label],
- assignees: [],
- });
-
- list.issues.push(issue);
- listDup.issues.push(issue);
-
- jest.spyOn(boardsStore, 'moveIssue');
-
- listDup.updateIssueLabel(issue, list);
-
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(
- issue.id,
- list.id,
- listDup.id,
- undefined,
- undefined,
- );
- });
-
- describe('page number', () => {
- beforeEach(() => {
- jest.spyOn(list, 'getIssues').mockImplementation(() => {});
- list.issues = [];
- });
-
- it('increase page number if current issue count is more than the page size', () => {
- for (let i = 0; i < 30; i += 1) {
- list.issues.push(
- new ListIssue({
- title: 'Testing',
- id: i,
- iid: i,
- confidential: false,
- labels: [list.label],
- assignees: [],
- }),
- );
- }
- list.issuesSize = 50;
-
- expect(list.issues.length).toBe(30);
-
- list.nextPage();
-
- expect(list.page).toBe(2);
- expect(list.getIssues).toHaveBeenCalled();
- });
-
- it('does not increase page number if issue count is less than the page size', () => {
- list.issues.push(
- new ListIssue({
- title: 'Testing',
- id: 1,
- confidential: false,
- labels: [list.label],
- assignees: [],
- }),
- );
- list.issuesSize = 2;
-
- list.nextPage();
-
- expect(list.page).toBe(1);
- expect(list.getIssues).toHaveBeenCalled();
- });
- });
-
- describe('newIssue', () => {
- beforeEach(() => {
- jest.spyOn(boardsStore, 'newIssue').mockReturnValue(
- Promise.resolve({
- data: {
- id: 42,
- subscribed: false,
- assignable_labels_endpoint: '/issue/42/labels',
- toggle_subscription_endpoint: '/issue/42/subscriptions',
- issue_sidebar_endpoint: '/issue/42/sidebar_info',
- },
- }),
- );
- list.issues = [];
- });
-
- it('adds new issue to top of list', (done) => {
- const user = new ListAssignee({
- id: 1,
- name: 'testing 123',
- username: 'test',
- avatar: 'test_image',
- });
-
- list.issues.push(
- new ListIssue({
- title: 'Testing',
- id: 1,
- confidential: false,
- labels: [new ListLabel(list.label)],
- assignees: [],
- }),
- );
- const dummyIssue = new ListIssue({
- title: 'new issue',
- id: 2,
- confidential: false,
- labels: [new ListLabel(list.label)],
- assignees: [user],
- subscribed: false,
- });
-
- list
- .newIssue(dummyIssue)
- .then(() => {
- expect(list.issues.length).toBe(2);
- expect(list.issues[0]).toBe(dummyIssue);
- expect(list.issues[0].subscribed).toBe(false);
- expect(list.issues[0].assignableLabelsEndpoint).toBe('/issue/42/labels');
- expect(list.issues[0].toggleSubscriptionEndpoint).toBe('/issue/42/subscriptions');
- expect(list.issues[0].sidebarInfoEndpoint).toBe('/issue/42/sidebar_info');
- expect(list.issues[0].labels).toBe(dummyIssue.labels);
- expect(list.issues[0].assignees).toBe(dummyIssue.assignees);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 106f7b04c4b..6a4f344bbfb 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,12 +1,8 @@
-/* global List */
-
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
-import Vue from 'vue';
-import '~/boards/models/list';
import { ListType } from '~/boards/constants';
-import boardsStore from '~/boards/stores/boards_store';
import { __ } from '~/locale';
+import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -196,8 +192,7 @@ export const mockIssue = {
export const mockActiveIssue = {
...mockIssue,
- fullId: 'gid://gitlab/Issue/436',
- id: 436,
+ id: 'gid://gitlab/Issue/436',
iid: '27',
subscribed: false,
emailsDisabled: false,
@@ -289,20 +284,6 @@ export const boardsMockInterceptor = (config) => {
return [200, body];
};
-export const setMockEndpoints = (opts = {}) => {
- const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/-/boards.json';
- const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
- const bulkUpdatePath = opts.bulkUpdatePath || '';
- const boardId = opts.boardId || '1';
-
- boardsStore.setEndpoints({
- boardsEndpoint,
- listsEndpoint,
- bulkUpdatePath,
- boardId,
- });
-};
-
export const mockList = {
id: 'gid://gitlab/List/1',
title: 'Open',
@@ -335,14 +316,26 @@ export const mockLabelList = {
issuesCount: 0,
};
+export const mockMilestoneList = {
+ id: 'gid://gitlab/List/3',
+ title: 'To Do',
+ position: 0,
+ listType: 'milestone',
+ collapsed: false,
+ label: null,
+ assignee: null,
+ milestone: {
+ webUrl: 'https://gitlab.com/h5bp/html5-boilerplate/-/milestones/1',
+ title: 'Backlog',
+ },
+ loading: false,
+ issuesCount: 0,
+};
+
export const mockLists = [mockList, mockLabelList];
export const mockListsById = keyBy(mockLists, 'id');
-export const mockListsWithModel = mockLists.map((listMock) =>
- Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
-);
-
export const mockIssuesByListId = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue3.id, mockIssue4.id],
'gid://gitlab/List/2': mockIssues.map(({ id }) => id),
@@ -547,17 +540,17 @@ export const mockMoveData = {
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
{
- icon: 'labels',
- title: __('Label'),
- type: 'label_name',
+ icon: 'user',
+ title: __('Assignee'),
+ type: 'assignee_username',
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
- token: LabelToken,
- unique: false,
- symbol: '~',
- fetchLabels,
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ preloadedAuthors: [],
},
{
icon: 'pencil',
@@ -574,17 +567,27 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
preloadedAuthors: [],
},
{
- icon: 'user',
- title: __('Assignee'),
- type: 'assignee_username',
+ icon: 'labels',
+ title: __('Label'),
+ type: 'label_name',
operators: [
{ value: '=', description: 'is' },
{ value: '!=', description: 'is not' },
],
- token: AuthorToken,
+ token: LabelToken,
+ unique: false,
+ symbol: '~',
+ fetchLabels,
+ },
+ {
+ icon: 'clock',
+ title: __('Milestone'),
+ symbol: '%',
+ type: 'milestone_title',
+ token: MilestoneToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: [],
+ defaultMilestones: DEFAULT_MILESTONES_GRAPHQL,
+ fetchMilestones,
},
{
icon: 'issues',
@@ -599,16 +602,6 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
],
},
{
- icon: 'clock',
- title: __('Milestone'),
- symbol: '%',
- type: 'milestone_title',
- token: MilestoneToken,
- unique: true,
- defaultMilestones: [],
- fetchMilestones,
- },
- {
icon: 'weight',
title: __('Weight'),
type: 'weight',
diff --git a/spec/frontend/boards/project_select_deprecated_spec.js b/spec/frontend/boards/project_select_deprecated_spec.js
deleted file mode 100644
index 4494de43083..00000000000
--- a/spec/frontend/boards/project_select_deprecated_spec.js
+++ /dev/null
@@ -1,263 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import axios from 'axios';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import ProjectSelect from '~/boards/components/project_select_deprecated.vue';
-import { ListType } from '~/boards/constants';
-import eventHub from '~/boards/eventhub';
-import createFlash from '~/flash';
-import httpStatus from '~/lib/utils/http_status';
-import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
-
-import { listObj, mockRawGroupProjects } from './mock_data';
-
-jest.mock('~/boards/eventhub');
-jest.mock('~/flash');
-
-const dummyGon = {
- api_version: 'v4',
- relative_url_root: '/gitlab',
-};
-
-const mockGroupId = 1;
-const mockProjectsList1 = mockRawGroupProjects.slice(0, 1);
-const mockProjectsList2 = mockRawGroupProjects.slice(1);
-const mockDefaultFetchOptions = {
- with_issues_enabled: true,
- with_shared: false,
- include_subgroups: true,
- order_by: 'similarity',
- archived: false,
-};
-
-const itemsPerPage = 20;
-
-describe('ProjectSelect component', () => {
- let wrapper;
- let axiosMock;
-
- const findLabel = () => wrapper.find("[data-testid='header-label']");
- const findGlDropdown = () => wrapper.find(GlDropdown);
- const findGlDropdownLoadingIcon = () =>
- findGlDropdown().find('button:first-child').find(GlLoadingIcon);
- const findGlSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
- const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem);
- const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
- const findInMenuLoadingIcon = () => wrapper.find("[data-testid='dropdown-text-loading-icon']");
- const findEmptySearchMessage = () => wrapper.find("[data-testid='empty-result-message']");
-
- const mockGetRequest = (data = [], statusCode = httpStatus.OK) => {
- axiosMock
- .onGet(`/gitlab/api/v4/groups/${mockGroupId}/projects.json`)
- .replyOnce(statusCode, data);
- };
-
- const searchForProject = async (keyword, waitForAll = true) => {
- findGlSearchBoxByType().vm.$emit('input', keyword);
-
- if (waitForAll) {
- await axios.waitForAll();
- }
- };
-
- const createWrapper = async ({ list = listObj } = {}, waitForAll = true) => {
- wrapper = mount(ProjectSelect, {
- propsData: {
- list,
- },
- provide: {
- groupId: 1,
- },
- });
-
- if (waitForAll) {
- await axios.waitForAll();
- }
- };
-
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- window.gon = dummyGon;
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- axiosMock.restore();
- jest.clearAllMocks();
- });
-
- it('displays a header title', async () => {
- createWrapper({});
-
- expect(findLabel().text()).toBe('Projects');
- });
-
- it('renders a default dropdown text', async () => {
- createWrapper({});
-
- expect(findGlDropdown().exists()).toBe(true);
- expect(findGlDropdown().text()).toContain('Select a project');
- });
-
- describe('when mounted', () => {
- it('displays a loading icon while projects are being fetched', async () => {
- mockGetRequest([]);
-
- createWrapper({}, false);
-
- expect(findGlDropdownLoadingIcon().exists()).toBe(true);
-
- await axios.waitForAll();
-
- expect(axiosMock.history.get[0].params).toMatchObject({ search: '' });
- expect(axiosMock.history.get[0].url).toBe(
- `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
- );
-
- expect(findGlDropdownLoadingIcon().exists()).toBe(false);
- });
- });
-
- describe('when dropdown menu is open', () => {
- describe('by default', () => {
- beforeEach(async () => {
- mockGetRequest(mockProjectsList1);
-
- await createWrapper();
- });
-
- it('shows GlSearchBoxByType with default attributes', () => {
- expect(findGlSearchBoxByType().exists()).toBe(true);
- expect(findGlSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search projects',
- debounce: '250',
- });
- });
-
- it("displays the fetched project's name", () => {
- expect(findFirstGlDropdownItem().exists()).toBe(true);
- expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList1[0].name);
- });
-
- it("doesn't render loading icon in the menu", () => {
- expect(findInMenuLoadingIcon().isVisible()).toBe(false);
- });
-
- it('renders empty search result message', async () => {
- await createWrapper();
-
- expect(findEmptySearchMessage().exists()).toBe(true);
- });
- });
-
- describe('when a project is selected', () => {
- beforeEach(async () => {
- mockGetRequest(mockProjectsList1);
-
- await createWrapper();
-
- await findFirstGlDropdownItem().find('button').trigger('click');
- });
-
- it('emits setSelectedProject with correct project metadata', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith('setSelectedProject', {
- id: mockProjectsList1[0].id,
- path: mockProjectsList1[0].path_with_namespace,
- name: mockProjectsList1[0].name,
- namespacedName: mockProjectsList1[0].name_with_namespace,
- });
- });
-
- it('renders the name of the selected project', () => {
- expect(findGlDropdown().find('.gl-new-dropdown-button-text').text()).toBe(
- mockProjectsList1[0].name,
- );
- });
- });
-
- describe('when user searches for a project', () => {
- beforeEach(async () => {
- mockGetRequest(mockProjectsList1);
-
- await createWrapper();
- });
-
- it('calls API with correct parameters with default fetch options', async () => {
- await searchForProject('foobar');
-
- const expectedApiParams = {
- search: 'foobar',
- per_page: itemsPerPage,
- ...mockDefaultFetchOptions,
- };
-
- expect(axiosMock.history.get[1].params).toMatchObject(expectedApiParams);
- expect(axiosMock.history.get[1].url).toBe(
- `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
- );
- });
-
- describe("when list type is defined and isn't backlog", () => {
- it('calls API with an additional fetch option (min_access_level)', async () => {
- axiosMock.reset();
-
- await createWrapper({ list: { ...listObj, type: ListType.label } });
-
- await searchForProject('foobar');
-
- const expectedApiParams = {
- search: 'foobar',
- per_page: itemsPerPage,
- ...mockDefaultFetchOptions,
- min_access_level: featureAccessLevel.EVERYONE,
- };
-
- expect(axiosMock.history.get[1].params).toMatchObject(expectedApiParams);
- expect(axiosMock.history.get[1].url).toBe(
- `/gitlab/api/v4/groups/${mockGroupId}/projects.json`,
- );
- });
- });
-
- it('displays and hides gl-loading-icon while and after fetching data', async () => {
- await searchForProject('some keyword', false);
-
- await wrapper.vm.$nextTick();
-
- expect(findInMenuLoadingIcon().isVisible()).toBe(true);
-
- await axios.waitForAll();
-
- expect(findInMenuLoadingIcon().isVisible()).toBe(false);
- });
-
- it('flashes an error message when fetching fails', async () => {
- mockGetRequest([], httpStatus.INTERNAL_SERVER_ERROR);
-
- await searchForProject('foobar');
-
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
- message: 'Something went wrong while fetching projects',
- });
- });
-
- describe('with non-empty search result', () => {
- beforeEach(async () => {
- mockGetRequest(mockProjectsList2);
-
- await searchForProject('foobar');
- });
-
- it('displays the retrieved list of projects', async () => {
- expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList2[0].name);
- });
-
- it('does not render empty search result message', async () => {
- expect(findEmptySearchMessage().exists()).toBe(false);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 1272a573d2f..62e0fa7a68a 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -26,7 +26,6 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
mockLists,
@@ -107,12 +106,7 @@ describe('setFilters', () => {
});
describe('performSearch', () => {
- it('should dispatch setFilters action', (done) => {
- testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done);
- });
-
- it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', (done) => {
- window.gon = { features: { graphqlBoardLists: true } };
+ it('should dispatch setFilters, fetchLists and resetIssues action', (done) => {
testAction(
actions.performSearch,
{},
@@ -496,12 +490,9 @@ describe('fetchLabels', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const commit = jest.fn();
- const getters = {
- shouldUseGraphQL: () => true,
- };
const state = { boardType: 'group' };
- await actions.fetchLabels({ getters, state, commit });
+ await actions.fetchLabels({ state, commit });
expect(commit).toHaveBeenCalledWith(types.RECEIVE_LABELS_SUCCESS, labels);
});
@@ -954,7 +945,7 @@ describe('moveIssue', () => {
});
describe('moveIssueCard and undoMoveIssueCard', () => {
- describe('card should move without clonning', () => {
+ describe('card should move without cloning', () => {
let state;
let params;
let moveMutations;
@@ -1221,8 +1212,8 @@ describe('updateMovedIssueCard', () => {
describe('updateIssueOrder', () => {
const issues = {
- 436: mockIssue,
- 437: mockIssue2,
+ [mockIssue.id]: mockIssue,
+ [mockIssue2.id]: mockIssue2,
};
const state = {
@@ -1231,7 +1222,7 @@ describe('updateIssueOrder', () => {
};
const moveData = {
- itemId: 436,
+ itemId: mockIssue.id,
fromListId: 'gid://gitlab/List/1',
toListId: 'gid://gitlab/List/2',
};
@@ -1490,7 +1481,7 @@ describe('addListNewIssue', () => {
type: 'addListItem',
payload: {
list: fakeList,
- item: formatIssue({ ...mockIssue, id: getIdFromGraphQLId(mockIssue.id) }),
+ item: formatIssue(mockIssue),
position: 0,
},
},
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index c0774dd3ae1..b30968c45d7 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -77,12 +77,12 @@ describe('Boards - Getters', () => {
});
describe('getBoardItemById', () => {
- const state = { boardItems: { 1: 'issue' } };
+ const state = { boardItems: { 'gid://gitlab/Issue/1': 'issue' } };
it.each`
- id | expected
- ${'1'} | ${'issue'}
- ${''} | ${{}}
+ id | expected
+ ${'gid://gitlab/Issue/1'} | ${'issue'}
+ ${''} | ${{}}
`('returns $expected when $id is passed to state', ({ id, expected }) => {
expect(getters.getBoardItemById(state)(id)).toEqual(expected);
});
@@ -90,11 +90,11 @@ describe('Boards - Getters', () => {
describe('activeBoardItem', () => {
it.each`
- id | expected
- ${'1'} | ${'issue'}
- ${''} | ${{ id: '', iid: '', fullId: '' }}
+ id | expected
+ ${'gid://gitlab/Issue/1'} | ${'issue'}
+ ${''} | ${{ id: '', iid: '' }}
`('returns $expected when $id is passed to state', ({ id, expected }) => {
- const state = { boardItems: { 1: 'issue' }, activeId: id };
+ const state = { boardItems: { 'gid://gitlab/Issue/1': 'issue' }, activeId: id };
expect(getters.activeBoardItem(state)).toEqual(expected);
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index a2ba1e9eb5e..0e830258327 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -407,7 +407,7 @@ describe('Board Store Mutations', () => {
describe('MUTATE_ISSUE_SUCCESS', () => {
it('updates issue in issues state', () => {
const issues = {
- 436: { id: rawIssue.id },
+ [rawIssue.id]: { id: rawIssue.id },
};
state = {
@@ -419,7 +419,7 @@ describe('Board Store Mutations', () => {
issue: rawIssue,
});
- expect(state.boardItems).toEqual({ 436: { ...mockIssue, id: 436 } });
+ expect(state.boardItems).toEqual({ [mockIssue.id]: mockIssue });
});
});
@@ -545,7 +545,7 @@ describe('Board Store Mutations', () => {
expect(state.groupProjectsFlags.isLoading).toBe(true);
});
- it('Should set isLoading in groupProjectsFlags to true in state when fetchNext is true', () => {
+ it('Should set isLoadingMore in groupProjectsFlags to true in state when fetchNext is true', () => {
mutations[types.REQUEST_GROUP_PROJECTS](state, true);
expect(state.groupProjectsFlags.isLoadingMore).toBe(true);
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 0e1fe790771..b34265b7234 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
@@ -47,8 +47,26 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
<!---->
<div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5"
+ >
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+ </div>
+
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+ </div>
+ </div>
+
+ <div
class="gl-new-dropdown-contents"
>
+ <!---->
+
<li
class="gl-new-dropdown-item"
role="presentation"
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index 271c6356f7e..c2fa6556847 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -17,11 +17,15 @@ exports[`Confidential merge request project form group component renders empty s
No forks are available to you.
<br />
-
- <gl-sprintf-stub
- message="To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private."
- />
-
+ To protect this issue's confidentiality,
+ <a
+ class="help-link"
+ href="https://test.com"
+ target="_blank"
+ >
+ fork this project
+ </a>
+ and set the fork's visibility to private.
<gl-link-stub
class="w-auto p-0 d-inline-block text-primary bg-transparent"
href="/help"
@@ -52,18 +56,16 @@ exports[`Confidential merge request project form group component renders fork dr
</label>
<div>
- <!---->
+ <dropdown-stub
+ projects="[object Object],[object Object]"
+ selectedproject="[object Object]"
+ />
<p
class="text-muted mt-1 mb-0"
>
- No forks are available to you.
- <br />
-
- <gl-sprintf-stub
- message="To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private."
- />
+ To protect this issue's confidentiality, a private fork of this project was selected.
<gl-link-stub
class="w-auto p-0 d-inline-block text-primary bg-transparent"
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
index 67f6d360f52..0e73d50fdb5 100644
--- a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -1,3 +1,4 @@
+import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
@@ -21,55 +22,52 @@ const mockData = [
},
},
];
-let vm;
+let wrapper;
let mock;
function factory(projects = mockData) {
mock = new MockAdapter(axios);
mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
- vm = shallowMount(ProjectFormGroup, {
+ wrapper = shallowMount(ProjectFormGroup, {
propsData: {
namespacePath: 'gitlab-org',
projectPath: 'gitlab-org/gitlab-ce',
newForkPath: 'https://test.com',
helpPagePath: '/help',
},
+ stubs: { GlSprintf },
});
+
+ return axios.waitForAll();
}
describe('Confidential merge request project form group component', () => {
afterEach(() => {
mock.restore();
- vm.destroy();
+ wrapper.destroy();
});
- it('renders fork dropdown', () => {
- factory();
+ it('renders fork dropdown', async () => {
+ await factory();
- return vm.vm.$nextTick(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
});
- it('sets selected project as first fork', () => {
- factory();
+ it('sets selected project as first fork', async () => {
+ await factory();
- return vm.vm.$nextTick(() => {
- expect(vm.vm.selectedProject).toEqual({
- id: 1,
- name: 'root / gitlab-ce',
- pathWithNamespace: 'root/gitlab-ce',
- namespaceFullpath: 'root',
- });
+ expect(wrapper.vm.selectedProject).toEqual({
+ id: 1,
+ name: 'root / gitlab-ce',
+ pathWithNamespace: 'root/gitlab-ce',
+ namespaceFullpath: 'root',
});
});
- it('renders empty state when response is empty', () => {
- factory([]);
+ it('renders empty state when response is empty', async () => {
+ await factory([]);
- return vm.vm.$nextTick(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
});
});
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 3c88c05a4b4..8f5516545eb 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -11,7 +11,16 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
<ul role=\\"menu\\" tabindex=\\"-1\\" class=\\"dropdown-menu\\">
<div class=\\"gl-new-dropdown-inner\\">
<!---->
+ <div class=\\"gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5\\">
+ <div class=\\"gl-display-flex\\">
+ <!---->
+ </div>
+ <div class=\\"gl-display-flex\\">
+ <!---->
+ </div>
+ </div>
<div class=\\"gl-new-dropdown-contents\\">
+ <!---->
<li role=\\"presentation\\" class=\\"gl-px-3!\\">
<form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
<div placeholder=\\"Link URL\\">
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index d516baf6f0f..3d1ef03083d 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -6,6 +6,7 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
+import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import {
LOADING_CONTENT_EVENT,
@@ -25,6 +26,7 @@ describe('ContentEditor', () => {
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findEditorContent = () => wrapper.findComponent(EditorContent);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findBubbleMenu = () => wrapper.findComponent(FormattingBubbleMenu);
const createWrapper = (propsData = {}) => {
renderMarkdown = jest.fn();
@@ -131,6 +133,10 @@ describe('ContentEditor', () => {
it('hides EditorContent component', () => {
expect(findEditorContent().exists()).toBe(false);
});
+
+ it('hides formatting bubble menu', () => {
+ expect(findBubbleMenu().exists()).toBe(false);
+ });
});
describe('when loading content succeeds', () => {
@@ -171,5 +177,9 @@ describe('ContentEditor', () => {
it('displays EditorContent component', () => {
expect(findEditorContent().exists()).toBe(true);
});
+
+ it('displays formatting bubble menu', () => {
+ expect(findBubbleMenu().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
new file mode 100644
index 00000000000..e48f59f6d9c
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -0,0 +1,193 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { NodeViewWrapper } from '@tiptap/vue-2';
+import { selectedRect as getSelectedRect } from 'prosemirror-tables';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
+import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
+
+jest.mock('prosemirror-tables');
+
+describe('content/components/wrappers/table_cell_base', () => {
+ let wrapper;
+ let editor;
+ let getPos;
+
+ const createWrapper = async (propsData = { cellType: 'td' }) => {
+ wrapper = shallowMountExtended(TableCellBaseWrapper, {
+ propsData: {
+ editor,
+ getPos,
+ ...propsData,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItemWithLabel = (name) =>
+ wrapper
+ .findAllComponents(GlDropdownItem)
+ .filter((dropdownItem) => dropdownItem.text().includes(name))
+ .at(0);
+ const findDropdownItemWithLabelExists = (name) =>
+ wrapper
+ .findAllComponents(GlDropdownItem)
+ .filter((dropdownItem) => dropdownItem.text().includes(name)).length > 0;
+ const setCurrentPositionInCell = () => {
+ const { $cursor } = editor.state.selection;
+
+ getPos.mockReturnValue($cursor.pos - $cursor.parentOffset - 1);
+ };
+ const mockDropdownHide = () => {
+ /*
+ * TODO: Replace this method with using the scoped hide function
+ * provided by BootstrapVue https://bootstrap-vue.org/docs/components/dropdown.
+ * GitLab UI is not exposing it in the default scope
+ */
+ findDropdown().vm.hide = jest.fn();
+ };
+
+ beforeEach(() => {
+ getPos = jest.fn();
+ editor = createTestEditor({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a td node-view-wrapper with relative position', () => {
+ createWrapper();
+ expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
+ expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('td');
+ });
+
+ it('displays dropdown when selection cursor is on the cell', async () => {
+ setCurrentPositionInCell();
+ createWrapper();
+
+ await nextTick();
+
+ expect(findDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ icon: 'chevron-down',
+ size: 'small',
+ split: false,
+ });
+ expect(findDropdown().attributes()).toMatchObject({
+ boundary: 'viewport',
+ 'no-caret': '',
+ });
+ });
+
+ it('does not display dropdown when selection cursor is not on the cell', async () => {
+ createWrapper();
+
+ await nextTick();
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ describe('when dropdown is visible', () => {
+ beforeEach(async () => {
+ setCurrentPositionInCell();
+ getSelectedRect.mockReturnValue({
+ map: {
+ height: 1,
+ width: 1,
+ },
+ });
+
+ createWrapper();
+ await nextTick();
+
+ mockDropdownHide();
+ });
+
+ it.each`
+ dropdownItemLabel | commandName
+ ${'Insert column before'} | ${'addColumnBefore'}
+ ${'Insert column after'} | ${'addColumnAfter'}
+ ${'Insert row before'} | ${'addRowBefore'}
+ ${'Insert row after'} | ${'addRowAfter'}
+ ${'Delete table'} | ${'deleteTable'}
+ `(
+ 'executes $commandName when $dropdownItemLabel button is clicked',
+ ({ commandName, dropdownItemLabel }) => {
+ const mocks = mockChainedCommands(editor, [commandName, 'run']);
+
+ findDropdownItemWithLabel(dropdownItemLabel).vm.$emit('click');
+
+ expect(mocks[commandName]).toHaveBeenCalled();
+ },
+ );
+
+ it('does not allow deleting rows and columns', async () => {
+ expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
+ expect(findDropdownItemWithLabelExists('Delete column')).toBe(false);
+ });
+
+ it('allows deleting rows when there are more than 2 rows in the table', async () => {
+ const mocks = mockChainedCommands(editor, ['deleteRow', 'run']);
+
+ getSelectedRect.mockReturnValue({
+ map: {
+ height: 3,
+ },
+ });
+
+ emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
+
+ await nextTick();
+
+ findDropdownItemWithLabel('Delete row').vm.$emit('click');
+
+ expect(mocks.deleteRow).toHaveBeenCalled();
+ });
+
+ it('allows deleting columns when there are more than 1 column in the table', async () => {
+ const mocks = mockChainedCommands(editor, ['deleteColumn', 'run']);
+
+ getSelectedRect.mockReturnValue({
+ map: {
+ width: 2,
+ },
+ });
+
+ emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
+
+ await nextTick();
+
+ findDropdownItemWithLabel('Delete column').vm.$emit('click');
+
+ expect(mocks.deleteColumn).toHaveBeenCalled();
+ });
+
+ describe('when current row is the table’s header', () => {
+ beforeEach(async () => {
+ // Remove 2 rows condition
+ getSelectedRect.mockReturnValue({
+ map: {
+ height: 3,
+ },
+ });
+
+ createWrapper({ cellType: 'th' });
+
+ await nextTick();
+ });
+
+ it('does not allow adding a row before the header', async () => {
+ expect(findDropdownItemWithLabelExists('Insert row before')).toBe(false);
+ });
+
+ it('does not allow removing the header row', async () => {
+ createWrapper({ cellType: 'th' });
+
+ await nextTick();
+
+ expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
new file mode 100644
index 00000000000..5d26c44ba03
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
@@ -0,0 +1,37 @@
+import { shallowMount } from '@vue/test-utils';
+import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
+import TableCellBodyWrapper from '~/content_editor/components/wrappers/table_cell_body.vue';
+import { createTestEditor } from '../../test_utils';
+
+describe('content/components/wrappers/table_cell_body', () => {
+ let wrapper;
+ let editor;
+ let getPos;
+
+ const createWrapper = async () => {
+ wrapper = shallowMount(TableCellBodyWrapper, {
+ propsData: {
+ editor,
+ getPos,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ getPos = jest.fn();
+ editor = createTestEditor({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a TableCellBase component', () => {
+ createWrapper();
+ expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
+ editor,
+ getPos,
+ cellType: 'td',
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
new file mode 100644
index 00000000000..e561191418d
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
@@ -0,0 +1,37 @@
+import { shallowMount } from '@vue/test-utils';
+import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
+import TableCellHeaderWrapper from '~/content_editor/components/wrappers/table_cell_header.vue';
+import { createTestEditor } from '../../test_utils';
+
+describe('content/components/wrappers/table_cell_header', () => {
+ let wrapper;
+ let editor;
+ let getPos;
+
+ const createWrapper = async () => {
+ wrapper = shallowMount(TableCellHeaderWrapper, {
+ propsData: {
+ editor,
+ getPos,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ getPos = jest.fn();
+ editor = createTestEditor({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a TableCellBase component', () => {
+ createWrapper();
+ expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
+ editor,
+ getPos,
+ cellType: 'th',
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index 1334b1ddaad..d4f05a25bd6 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -1,18 +1,23 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { once } from 'lodash';
-import waitForPromises from 'helpers/wait_for_promises';
import Attachment from '~/content_editor/extensions/attachment';
import Image from '~/content_editor/extensions/image';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
import httpStatus from '~/lib/utils/http_status';
-import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor, createDocBuilder } from '../test_utils';
+const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
+ <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png">
+ <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">
+ </a>
+</p>`;
+const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
+ <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
+</p>`;
+
describe('content_editor/extensions/attachment', () => {
let tiptapEditor;
- let eq;
let doc;
let p;
let image;
@@ -25,6 +30,24 @@ describe('content_editor/extensions/attachment', () => {
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' });
+ const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => {
+ return new Promise((resolve) => {
+ let counter = 1;
+ const handleTransaction = () => {
+ if (counter === number) {
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ tiptapEditor.off('update', handleTransaction);
+ resolve();
+ }
+
+ counter += 1;
+ };
+
+ tiptapEditor.on('update', handleTransaction);
+ action();
+ });
+ };
+
beforeEach(() => {
renderMarkdown = jest.fn();
@@ -34,7 +57,6 @@ describe('content_editor/extensions/attachment', () => {
({
builders: { doc, p, image, loading, link },
- eq,
} = createDocBuilder({
tiptapEditor,
names: {
@@ -76,9 +98,7 @@ describe('content_editor/extensions/attachment', () => {
const base64EncodedFile = 'data:image/png;base64,Zm9v';
beforeEach(() => {
- renderMarkdown.mockResolvedValue(
- loadMarkdownApiResult('project_wiki_attachment_image').body,
- );
+ renderMarkdown.mockResolvedValue(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
});
describe('when uploading succeeds', () => {
@@ -92,18 +112,14 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(httpStatus.OK, successResponse);
});
- it('inserts an image with src set to the encoded image file and uploading true', (done) => {
+ it('inserts an image with src set to the encoded image file and uploading true', async () => {
const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile })));
- tiptapEditor.on(
- 'update',
- once(() => {
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
- done();
- }),
- );
-
- tiptapEditor.commands.uploadAttachment({ file: imageFile });
+ await expectDocumentAfterTransaction({
+ number: 1,
+ expectedDoc,
+ action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
+ });
});
it('updates the inserted image with canonicalSrc when upload is successful', async () => {
@@ -118,11 +134,11 @@ describe('content_editor/extensions/attachment', () => {
),
);
- tiptapEditor.commands.uploadAttachment({ file: imageFile });
-
- await waitForPromises();
-
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ await expectDocumentAfterTransaction({
+ number: 2,
+ expectedDoc,
+ action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
+ });
});
});
@@ -131,14 +147,14 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR);
});
- it('resets the doc to orginal state', async () => {
+ it('resets the doc to original state', async () => {
const expectedDoc = doc(p(''));
- tiptapEditor.commands.uploadAttachment({ file: imageFile });
-
- await waitForPromises();
-
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ await expectDocumentAfterTransaction({
+ number: 2,
+ expectedDoc,
+ action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
+ });
});
it('emits an error event that includes an error message', (done) => {
@@ -153,7 +169,7 @@ describe('content_editor/extensions/attachment', () => {
});
describe('when the file has a zip (or any other attachment) mime type', () => {
- const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link').body;
+ const markdownApiResult = PROJECT_WIKI_ATTACHMENT_LINK_HTML;
beforeEach(() => {
renderMarkdown.mockResolvedValue(markdownApiResult);
@@ -170,18 +186,14 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(httpStatus.OK, successResponse);
});
- it('inserts a loading mark', (done) => {
+ it('inserts a loading mark', async () => {
const expectedDoc = doc(p(loading({ label: 'test-file' })));
- tiptapEditor.on(
- 'update',
- once(() => {
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
- done();
- }),
- );
-
- tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
+ await expectDocumentAfterTransaction({
+ number: 1,
+ expectedDoc,
+ action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
+ });
});
it('updates the loading mark with a link with canonicalSrc and href attrs', async () => {
@@ -198,11 +210,11 @@ describe('content_editor/extensions/attachment', () => {
),
);
- tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
-
- await waitForPromises();
-
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ await expectDocumentAfterTransaction({
+ number: 2,
+ expectedDoc,
+ action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
+ });
});
});
@@ -214,11 +226,11 @@ describe('content_editor/extensions/attachment', () => {
it('resets the doc to orginal state', async () => {
const expectedDoc = doc(p(''));
- tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
-
- await waitForPromises();
-
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ await expectDocumentAfterTransaction({
+ number: 2,
+ expectedDoc,
+ action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
+ });
});
it('emits an error event that includes an error message', (done) => {
diff --git a/spec/frontend/content_editor/extensions/blockquote_spec.js b/spec/frontend/content_editor/extensions/blockquote_spec.js
new file mode 100644
index 00000000000..c5b5044352d
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/blockquote_spec.js
@@ -0,0 +1,19 @@
+import { multilineInputRegex } from '~/content_editor/extensions/blockquote';
+
+describe('content_editor/extensions/blockquote', () => {
+ describe.each`
+ input | matches
+ ${'>>> '} | ${true}
+ ${' >>> '} | ${true}
+ ${'\t>>> '} | ${true}
+ ${'>> '} | ${false}
+ ${'>>>x '} | ${false}
+ ${'> '} | ${false}
+ `('multilineInputRegex', ({ input, matches }) => {
+ it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
+ const match = new RegExp(multilineInputRegex).test(input);
+
+ expect(match).toBe(matches);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index 188e6580dc6..6a0a0c76825 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -1,9 +1,15 @@
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor } from '../test_utils';
+const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true">
+ <code>
+ <span id="LC1" class="line" lang="javascript">
+ <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span>
+ </span>
+ </code>
+</pre>`;
+
describe('content_editor/extensions/code_block_highlight', () => {
- let codeBlockHtmlFixture;
let parsedCodeBlockHtmlFixture;
let tiptapEditor;
@@ -11,13 +17,10 @@ describe('content_editor/extensions/code_block_highlight', () => {
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
beforeEach(() => {
- const { html } = loadMarkdownApiResult('code_block');
-
tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
- codeBlockHtmlFixture = html;
- parsedCodeBlockHtmlFixture = parseHTML(codeBlockHtmlFixture);
+ parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML);
- tiptapEditor.commands.setContent(codeBlockHtmlFixture);
+ tiptapEditor.commands.setContent(CODE_BLOCK_HTML);
});
it('extracts language and params attributes from Markdown API output', () => {
diff --git a/spec/frontend/content_editor/markdown_processing_examples.js b/spec/frontend/content_editor/markdown_processing_examples.js
index 12eed00f3c6..b3aabfeb145 100644
--- a/spec/frontend/content_editor/markdown_processing_examples.js
+++ b/spec/frontend/content_editor/markdown_processing_examples.js
@@ -6,7 +6,8 @@ import { getJSONFixture } from 'helpers/fixtures';
export const loadMarkdownApiResult = (testName) => {
const fixturePathPrefix = `api/markdown/${testName}.json`;
- return getJSONFixture(fixturePathPrefix);
+ const fixture = getJSONFixture(fixturePathPrefix);
+ return fixture.body || fixture.html;
};
export const loadMarkdownApiExamples = () => {
@@ -16,3 +17,9 @@ export const loadMarkdownApiExamples = () => {
return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
};
+
+export const loadMarkdownApiExample = (testName) => {
+ return loadMarkdownApiExamples().find(([name, context]) => {
+ return (context ? `${context}_${name}` : name) === testName;
+ })[2];
+};
diff --git a/spec/frontend/content_editor/markdown_processing_spec.js b/spec/frontend/content_editor/markdown_processing_spec.js
index da3f6e64db8..71565768558 100644
--- a/spec/frontend/content_editor/markdown_processing_spec.js
+++ b/spec/frontend/content_editor/markdown_processing_spec.js
@@ -9,8 +9,9 @@ describe('markdown processing', () => {
'correctly handles %s (context: %s)',
async (name, context, markdown) => {
const testName = context ? `${context}_${name}` : name;
- const { html, body } = loadMarkdownApiResult(testName);
- const contentEditor = createContentEditor({ renderMarkdown: () => html || body });
+ const contentEditor = createContentEditor({
+ renderMarkdown: () => loadMarkdownApiResult(testName),
+ });
await contentEditor.setSerializedContent(markdown);
expect(contentEditor.getSerializedContent()).toBe(markdown);
diff --git a/spec/frontend/content_editor/services/mark_utils_spec.js b/spec/frontend/content_editor/services/mark_utils_spec.js
new file mode 100644
index 00000000000..bbfb8f26f99
--- /dev/null
+++ b/spec/frontend/content_editor/services/mark_utils_spec.js
@@ -0,0 +1,38 @@
+import {
+ markInputRegex,
+ extractMarkAttributesFromMatch,
+} from '~/content_editor/services/mark_utils';
+
+describe('content_editor/services/mark_utils', () => {
+ describe.each`
+ tag | input | matches
+ ${'tag'} | ${'<tag>hello</tag>'} | ${true}
+ ${'tag'} | ${'<tag title="tooltip">hello</tag>'} | ${true}
+ ${'kbd'} | ${'Hold <kbd>Ctrl</kbd>'} | ${true}
+ ${'time'} | ${'Lets meet at <time title="today" datetime="20:00">20:00</time>'} | ${true}
+ ${'tag'} | ${'<tag width=30 height=30>attrs not quoted</tag>'} | ${false}
+ ${'tag'} | ${"<tag title='abc'>single quote attrs not supported</tag>"} | ${false}
+ ${'tag'} | ${'<tag title>attr has no value</tag>'} | ${false}
+ ${'tag'} | ${'<tag>tag opened but not closed'} | ${false}
+ ${'tag'} | ${'</tag>tag closed before opened<tag>'} | ${false}
+ `('inputRegex("$tag")', ({ tag, input, matches }) => {
+ it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
+ const match = markInputRegex(tag).test(input);
+
+ expect(match).toBe(matches);
+ });
+ });
+
+ describe.each`
+ tag | input | attrs
+ ${'kbd'} | ${'Hold <kbd>Ctrl</kbd>'} | ${{}}
+ ${'tag'} | ${'<tag title="tooltip">hello</tag>'} | ${{ title: 'tooltip' }}
+ ${'time'} | ${'Lets meet at <time title="today" datetime="20:00">20:00</time>'} | ${{ title: 'today', datetime: '20:00' }}
+ ${'abbr'} | ${'Sure, you can try it out but <abbr title="Your mileage may vary">YMMV</abbr>'} | ${{ title: 'Your mileage may vary' }}
+ `('extractAttributesFromMatch(inputRegex("$tag").exec(\'$input\'))', ({ tag, input, attrs }) => {
+ it(`returns: "${JSON.stringify(attrs)}"`, () => {
+ const matches = markInputRegex(tag).exec(input);
+ expect(extractMarkAttributesFromMatch(matches)).toEqual(attrs);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
new file mode 100644
index 00000000000..6f2c908c289
--- /dev/null
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -0,0 +1,1008 @@
+import Blockquote from '~/content_editor/extensions/blockquote';
+import Bold from '~/content_editor/extensions/bold';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import Code from '~/content_editor/extensions/code';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import DescriptionItem from '~/content_editor/extensions/description_item';
+import DescriptionList from '~/content_editor/extensions/description_list';
+import Division from '~/content_editor/extensions/division';
+import Emoji from '~/content_editor/extensions/emoji';
+import Figure from '~/content_editor/extensions/figure';
+import FigureCaption from '~/content_editor/extensions/figure_caption';
+import HardBreak from '~/content_editor/extensions/hard_break';
+import Heading from '~/content_editor/extensions/heading';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import Image from '~/content_editor/extensions/image';
+import InlineDiff from '~/content_editor/extensions/inline_diff';
+import Italic from '~/content_editor/extensions/italic';
+import Link from '~/content_editor/extensions/link';
+import ListItem from '~/content_editor/extensions/list_item';
+import OrderedList from '~/content_editor/extensions/ordered_list';
+import Paragraph from '~/content_editor/extensions/paragraph';
+import Strike from '~/content_editor/extensions/strike';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableHeader from '~/content_editor/extensions/table_header';
+import TableRow from '~/content_editor/extensions/table_row';
+import TaskItem from '~/content_editor/extensions/task_item';
+import TaskList from '~/content_editor/extensions/task_list';
+import Text from '~/content_editor/extensions/text';
+import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+jest.mock('~/emoji');
+
+jest.mock('~/content_editor/services/feature_flags', () => ({
+ isBlockTablesFeatureEnabled: jest.fn().mockReturnValue(true),
+}));
+
+const tiptapEditor = createTestEditor({
+ extensions: [
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ DescriptionItem,
+ DescriptionList,
+ Division,
+ Emoji,
+ Figure,
+ FigureCaption,
+ HardBreak,
+ Heading,
+ HorizontalRule,
+ Image,
+ InlineDiff,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ Paragraph,
+ Strike,
+ Table,
+ TableCell,
+ TableHeader,
+ TableRow,
+ TaskItem,
+ TaskList,
+ Text,
+ ],
+});
+
+const {
+ builders: {
+ doc,
+ blockquote,
+ bold,
+ bulletList,
+ code,
+ codeBlock,
+ division,
+ descriptionItem,
+ descriptionList,
+ emoji,
+ figure,
+ figureCaption,
+ heading,
+ hardBreak,
+ horizontalRule,
+ image,
+ inlineDiff,
+ italic,
+ link,
+ listItem,
+ orderedList,
+ paragraph,
+ strike,
+ table,
+ tableCell,
+ tableHeader,
+ tableRow,
+ 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 },
+ division: { nodeType: Division.name },
+ descriptionItem: { nodeType: DescriptionItem.name },
+ descriptionList: { nodeType: DescriptionList.name },
+ emoji: { markType: Emoji.name },
+ figure: { nodeType: Figure.name },
+ figureCaption: { nodeType: FigureCaption.name },
+ hardBreak: { nodeType: HardBreak.name },
+ heading: { nodeType: Heading.name },
+ horizontalRule: { nodeType: HorizontalRule.name },
+ image: { nodeType: Image.name },
+ inlineDiff: { markType: InlineDiff.name },
+ italic: { nodeType: Italic.name },
+ link: { markType: Link.name },
+ listItem: { nodeType: ListItem.name },
+ orderedList: { nodeType: OrderedList.name },
+ paragraph: { nodeType: Paragraph.name },
+ strike: { markType: 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 },
+ },
+});
+
+const serialize = (...content) =>
+ markdownSerializer({}).serialize({
+ schema: tiptapEditor.schema,
+ content: doc(...content).toJSON(),
+ });
+
+describe('markdownSerializer', () => {
+ it('correctly serializes bold', () => {
+ expect(serialize(paragraph(bold('bold')))).toBe('**bold**');
+ });
+
+ it('correctly serializes italics', () => {
+ expect(serialize(paragraph(italic('italics')))).toBe('_italics_');
+ });
+
+ it('correctly serializes inline diff', () => {
+ expect(
+ serialize(
+ paragraph(
+ inlineDiff({ type: 'addition' }, '+30 lines'),
+ inlineDiff({ type: 'deletion' }, '-10 lines'),
+ ),
+ ),
+ ).toBe('{++30 lines+}{--10 lines-}');
+ });
+
+ it('correctly serializes a line break', () => {
+ expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
+ });
+
+ it('correctly serializes a link', () => {
+ expect(serialize(paragraph(link({ href: 'https://example.com' }, 'example url')))).toBe(
+ '[example url](https://example.com)',
+ );
+ });
+
+ it('correctly serializes a plain URL link', () => {
+ expect(serialize(paragraph(link({ href: 'https://example.com' }, 'https://example.com')))).toBe(
+ '<https://example.com>',
+ );
+ });
+
+ it('correctly serializes a link with a title', () => {
+ expect(
+ serialize(
+ paragraph(link({ href: 'https://example.com', title: 'click this link' }, 'example url')),
+ ),
+ ).toBe('[example url](https://example.com "click this link")');
+ });
+
+ it('correctly serializes a plain URL link with a title', () => {
+ expect(
+ serialize(
+ paragraph(
+ link({ href: 'https://example.com', title: 'link title' }, 'https://example.com'),
+ ),
+ ),
+ ).toBe('[https://example.com](https://example.com "link title")');
+ });
+
+ it('correctly serializes a link with a canonicalSrc', () => {
+ expect(
+ serialize(
+ paragraph(
+ link(
+ {
+ href: '/uploads/abcde/file.zip',
+ canonicalSrc: 'file.zip',
+ title: 'click here to download',
+ },
+ 'download file',
+ ),
+ ),
+ ),
+ ).toBe('[download file](file.zip "click here to download")');
+ });
+
+ it('correctly serializes strikethrough', () => {
+ expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
+ });
+
+ it('correctly serializes blockquotes with hard breaks', () => {
+ expect(serialize(blockquote('some text', hardBreak(), hardBreak(), 'new line'))).toBe(
+ `
+> some text\\
+> \\
+> new line
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes blockquote with multiple block nodes', () => {
+ expect(serialize(blockquote(paragraph('some paragraph'), codeBlock('var x = 10;')))).toBe(
+ `
+> some paragraph
+>
+> \`\`\`
+> var x = 10;
+> \`\`\`
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a multiline blockquote', () => {
+ expect(
+ serialize(
+ blockquote(
+ { multiline: true },
+ paragraph('some paragraph with ', bold('bold')),
+ codeBlock('var y = 10;'),
+ ),
+ ),
+ ).toBe(
+ `
+>>>
+some paragraph with **bold**
+
+\`\`\`
+var y = 10;
+\`\`\`
+
+>>>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a code block with language', () => {
+ expect(
+ serialize(
+ codeBlock(
+ { language: 'json' },
+ 'this is not really json but just trying out whether this case works or not',
+ ),
+ ),
+ ).toBe(
+ `
+\`\`\`json
+this is not really json but just trying out whether this case works or not
+\`\`\`
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes emoji', () => {
+ expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:');
+ });
+
+ it('correctly serializes headings', () => {
+ expect(
+ serialize(
+ heading({ level: 1 }, 'Heading 1'),
+ heading({ level: 2 }, 'Heading 2'),
+ heading({ level: 3 }, 'Heading 3'),
+ heading({ level: 4 }, 'Heading 4'),
+ heading({ level: 5 }, 'Heading 5'),
+ heading({ level: 6 }, 'Heading 6'),
+ ),
+ ).toBe(
+ `
+# Heading 1
+
+## Heading 2
+
+### Heading 3
+
+#### Heading 4
+
+##### Heading 5
+
+###### Heading 6
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes horizontal rule', () => {
+ expect(serialize(horizontalRule(), horizontalRule(), horizontalRule())).toBe(
+ `
+---
+
+---
+
+---
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes an image', () => {
+ expect(serialize(paragraph(image({ src: 'img.jpg', alt: 'foo bar' })))).toBe(
+ '![foo bar](img.jpg)',
+ );
+ });
+
+ it('correctly serializes an image with a title', () => {
+ expect(serialize(paragraph(image({ src: 'img.jpg', title: 'baz', alt: 'foo bar' })))).toBe(
+ '![foo bar](img.jpg "baz")',
+ );
+ });
+
+ it('correctly serializes an image with a canonicalSrc', () => {
+ expect(
+ serialize(
+ paragraph(
+ image({
+ src: '/uploads/abcde/file.png',
+ alt: 'this is an image',
+ canonicalSrc: 'file.png',
+ title: 'foo bar baz',
+ }),
+ ),
+ ),
+ ).toBe('![this is an image](file.png "foo bar baz")');
+ });
+
+ it('correctly serializes bullet list', () => {
+ expect(
+ serialize(
+ bulletList(
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+* list item 1
+* list item 2
+* list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes bullet list with different bullet styles', () => {
+ expect(
+ serialize(
+ bulletList(
+ { bullet: '+' },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(
+ paragraph('list item 3'),
+ bulletList(
+ { bullet: '-' },
+ listItem(paragraph('sub-list item 1')),
+ listItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
++ list item 1
++ list item 2
++ list item 3
+ - sub-list item 1
+ - sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list', () => {
+ expect(
+ serialize(
+ orderedList(
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+1. list item 1
+2. list item 2
+3. list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list with parens', () => {
+ expect(
+ serialize(
+ orderedList(
+ { parens: true },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+1) list item 1
+2) list item 2
+3) list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list with a different start order', () => {
+ expect(
+ serialize(
+ orderedList(
+ { start: 17 },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+17. list item 1
+18. list item 2
+19. list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric list with an invalid start order', () => {
+ expect(
+ serialize(
+ orderedList(
+ { start: NaN },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(paragraph('list item 3')),
+ ),
+ ),
+ ).toBe(
+ `
+1. list item 1
+2. list item 2
+3. list item 3
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a bullet list inside an ordered list', () => {
+ expect(
+ serialize(
+ orderedList(
+ { start: 17 },
+ listItem(paragraph('list item 1')),
+ listItem(paragraph('list item 2')),
+ listItem(
+ paragraph('list item 3'),
+ bulletList(
+ listItem(paragraph('sub-list item 1')),
+ listItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ // notice that 4 space indent works fine in this case,
+ // when it usually wouldn't
+ `
+17. list item 1
+18. list item 2
+19. list item 3
+ * sub-list item 1
+ * sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a task list', () => {
+ expect(
+ serialize(
+ taskList(
+ taskItem({ checked: true }, paragraph('list item 1')),
+ taskItem(paragraph('list item 2')),
+ taskItem(
+ paragraph('list item 3'),
+ taskList(
+ taskItem({ checked: true }, paragraph('sub-list item 1')),
+ taskItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+* [x] list item 1
+* [ ] list item 2
+* [ ] list item 3
+ * [x] sub-list item 1
+ * [ ] sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a numeric task list + with start order', () => {
+ expect(
+ serialize(
+ taskList(
+ { numeric: true },
+ taskItem({ checked: true }, paragraph('list item 1')),
+ taskItem(paragraph('list item 2')),
+ taskItem(
+ paragraph('list item 3'),
+ taskList(
+ { numeric: true, start: 1351, parens: true },
+ taskItem({ checked: true }, paragraph('sub-list item 1')),
+ taskItem(paragraph('sub-list item 2')),
+ ),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+1. [x] list item 1
+2. [ ] list item 2
+3. [ ] list item 3
+ 1351) [x] sub-list item 1
+ 1352) [ ] sub-list item 2
+ `.trim(),
+ );
+ });
+
+ it('correctly renders a description list', () => {
+ expect(
+ serialize(
+ descriptionList(
+ descriptionItem(paragraph('Beast of Bodmin')),
+ descriptionItem({ isTerm: false }, paragraph('A large feline inhabiting Bodmin Moor.')),
+
+ descriptionItem(paragraph('Morgawr')),
+ descriptionItem({ isTerm: false }, paragraph('A sea serpent.')),
+
+ descriptionItem(paragraph('Owlman')),
+ descriptionItem(
+ { isTerm: false },
+ paragraph('A giant ', italic('owl-like'), ' creature.'),
+ ),
+ ),
+ ),
+ ).toBe(
+ `
+<dl>
+<dt>Beast of Bodmin</dt>
+<dd>A large feline inhabiting Bodmin Moor.</dd>
+<dt>Morgawr</dt>
+<dd>A sea serpent.</dd>
+<dt>Owlman</dt>
+<dd>
+
+A giant _owl-like_ creature.
+
+</dd>
+</dl>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders div', () => {
+ expect(
+ serialize(
+ division(paragraph('just a paragraph in a div')),
+ division(paragraph('just some ', bold('styled'), ' ', italic('content'), ' in a div')),
+ ),
+ ).toBe(
+ '<div>just a paragraph in a div</div>\n<div>\n\njust some **styled** _content_ in a div\n\n</div>',
+ );
+ });
+
+ it('correctly renders figure', () => {
+ expect(
+ serialize(
+ figure(
+ paragraph(image({ src: 'elephant.jpg', alt: 'An elephant at sunset' })),
+ figureCaption('An elephant at sunset'),
+ ),
+ ),
+ ).toBe(
+ `
+<figure>
+
+![An elephant at sunset](elephant.jpg)
+
+<figcaption>An elephant at sunset</figcaption>
+</figure>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders figure with styled caption', () => {
+ expect(
+ serialize(
+ figure(
+ paragraph(image({ src: 'elephant.jpg', alt: 'An elephant at sunset' })),
+ figureCaption(italic('An elephant at sunset')),
+ ),
+ ),
+ ).toBe(
+ `
+<figure>
+
+![An elephant at sunset](elephant.jpg)
+
+<figcaption>
+
+_An elephant at sunset_
+
+</figcaption>
+</figure>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with inline content', () => {
+ expect(
+ serialize(
+ table(
+ // each table cell must contain at least one paragraph
+ tableRow(
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ ),
+ tableRow(
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ ),
+ tableRow(
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ tableCell(paragraph('cell')),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header | header |
+|--------|--------|--------|
+| cell | cell | cell |
+| cell | cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with line breaks', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(
+ tableCell(paragraph('cell with', hardBreak(), 'line', hardBreak(), 'breaks')),
+ tableCell(paragraph('cell')),
+ ),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header |
+|--------|--------|
+| cell with<br>line<br>breaks | cell |
+| cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes two consecutive tables', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ table(
+ tableRow(tableHeader(paragraph('header')), tableHeader(paragraph('header'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+| header | header |
+|--------|--------|
+| cell | cell |
+| cell | cell |
+
+| header | header |
+|--------|--------|
+| cell | cell |
+| cell | cell |
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes a table with block content', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(
+ tableHeader(paragraph('examples of')),
+ tableHeader(paragraph('block content')),
+ tableHeader(paragraph('in tables')),
+ tableHeader(paragraph('in content editor')),
+ ),
+ tableRow(
+ tableCell(heading({ level: 1 }, 'heading 1')),
+ tableCell(heading({ level: 2 }, 'heading 2')),
+ tableCell(paragraph(bold('just bold'))),
+ tableCell(paragraph(bold('bold'), ' ', italic('italic'), ' ', code('code'))),
+ ),
+ tableRow(
+ tableCell(
+ paragraph('all marks in three paragraphs:'),
+ paragraph('the ', bold('quick'), ' ', italic('brown'), ' ', code('fox')),
+ paragraph(
+ link({ href: '/home' }, 'jumps'),
+ ' over the ',
+ strike('lazy'),
+ ' ',
+ emoji({ name: 'dog' }),
+ ),
+ ),
+ tableCell(
+ paragraph(image({ src: 'img.jpg', alt: 'some image' }), hardBreak(), 'image content'),
+ ),
+ tableCell(
+ blockquote('some text', hardBreak(), hardBreak(), 'in a multiline blockquote'),
+ ),
+ tableCell(
+ codeBlock(
+ { language: 'javascript' },
+ 'var a = 2;\nvar b = 3;\nvar c = a + d;\n\nconsole.log(c);',
+ ),
+ ),
+ ),
+ tableRow(
+ tableCell(bulletList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
+ tableCell(orderedList(listItem('item 1'), listItem('item 2'), listItem('item 2'))),
+ tableCell(
+ paragraph('paragraphs separated by'),
+ horizontalRule(),
+ paragraph('a horizontal rule'),
+ ),
+ tableCell(
+ table(
+ tableRow(tableHeader(paragraph('table')), tableHeader(paragraph('inside'))),
+ tableRow(tableCell(paragraph('another')), tableCell(paragraph('table'))),
+ ),
+ ),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>examples of</th>
+<th>block content</th>
+<th>in tables</th>
+<th>in content editor</th>
+</tr>
+<tr>
+<td>
+
+# heading 1
+</td>
+<td>
+
+## heading 2
+</td>
+<td>
+
+**just bold**
+</td>
+<td>
+
+**bold** _italic_ \`code\`
+</td>
+</tr>
+<tr>
+<td>
+
+all marks in three paragraphs:
+
+the **quick** _brown_ \`fox\`
+
+[jumps](/home) over the ~~lazy~~ :dog:
+</td>
+<td>
+
+![some image](img.jpg)<br>image content
+</td>
+<td>
+
+> some text\\
+> \\
+> in a multiline blockquote
+</td>
+<td>
+
+\`\`\`javascript
+var a = 2;
+var b = 3;
+var c = a + d;
+
+console.log(c);
+\`\`\`
+</td>
+</tr>
+<tr>
+<td>
+
+* item 1
+* item 2
+* item 2
+</td>
+<td>
+
+1. item 1
+2. item 2
+3. item 2
+</td>
+<td>
+
+paragraphs separated by
+
+---
+
+a horizontal rule
+</td>
+<td>
+
+| table | inside |
+|-------|--------|
+| another | table |
+
+</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly renders content after a markdown table', () => {
+ expect(
+ serialize(
+ table(tableRow(tableHeader(paragraph('header'))), tableRow(tableCell(paragraph('cell')))),
+ heading({ level: 1 }, 'this is a heading'),
+ ).trim(),
+ ).toBe(
+ `
+| header |
+|--------|
+| cell |
+
+# this is a heading
+ `.trim(),
+ );
+ });
+
+ it('correctly renders content after an html table', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('header'))),
+ tableRow(tableCell(blockquote('hi'), paragraph('there'))),
+ ),
+ heading({ level: 1 }, 'this is a heading'),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>header</th>
+</tr>
+<tr>
+<td>
+
+> hi
+
+there
+</td>
+</tr>
+</table>
+
+# this is a heading
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes tables with misplaced header cells', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableHeader(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableHeader(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>cell</th>
+<td>cell</td>
+</tr>
+<tr>
+<td>cell</td>
+<th>cell</th>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes table without any headers', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ tableRow(tableCell(paragraph('cell')), tableCell(paragraph('cell'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<td>cell</td>
+<td>cell</td>
+</tr>
+<tr>
+<td>cell</td>
+<td>cell</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
+ it('correctly serializes table with rowspan and colspan', () => {
+ expect(
+ serialize(
+ table(
+ tableRow(
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ tableHeader(paragraph('header')),
+ ),
+ tableRow(
+ tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2')),
+ tableCell({ rowspan: 2 }, paragraph('cell')),
+ ),
+ tableRow(tableCell({ colspan: 2 }, paragraph('cell with rowspan: 2'))),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>header</th>
+<th>header</th>
+<th>header</th>
+</tr>
+<tr>
+<td colspan="2">cell with rowspan: 2</td>
+<td rowspan="2">cell</td>
+</tr>
+<tr>
+<td colspan="2">cell with rowspan: 2</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+});
diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
new file mode 100644
index 00000000000..6f908f468f6
--- /dev/null
+++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js
@@ -0,0 +1,81 @@
+import { Extension } from '@tiptap/core';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import ListItem from '~/content_editor/extensions/list_item';
+import Paragraph from '~/content_editor/extensions/paragraph';
+import markdownSerializer from '~/content_editor/services/markdown_serializer';
+import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+const BULLET_LIST_MARKDOWN = `+ list item 1
++ list item 2
+ - embedded list item 3`;
+const BULLET_LIST_HTML = `<ul data-sourcepos="1:1-3:24" dir="auto">
+ <li data-sourcepos="1:1-1:13">list item 1</li>
+ <li data-sourcepos="2:1-3:24">list item 2
+ <ul data-sourcepos="3:3-3:24">
+ <li data-sourcepos="3:3-3:24">embedded list item 3</li>
+ </ul>
+ </li>
+</ul>`;
+
+const SourcemapExtension = Extension.create({
+ // lets add `source` attribute to every element using `getMarkdownSource`
+ addGlobalAttributes() {
+ return [
+ {
+ types: [Paragraph.name, BulletList.name, ListItem.name],
+ attributes: {
+ source: {
+ parseHTML: (element) => {
+ const source = getMarkdownSource(element);
+ return source;
+ },
+ },
+ },
+ },
+ ];
+ },
+});
+
+const tiptapEditor = createTestEditor({
+ extensions: [BulletList, ListItem, SourcemapExtension],
+});
+
+const {
+ builders: { doc, bulletList, listItem, paragraph },
+} = createDocBuilder({
+ tiptapEditor,
+ names: {
+ bulletList: { nodeType: BulletList.name },
+ listItem: { nodeType: ListItem.name },
+ },
+});
+
+describe('content_editor/services/markdown_sourcemap', () => {
+ it('gets markdown source for a rendered HTML element', async () => {
+ const deserialized = await markdownSerializer({
+ render: () => BULLET_LIST_HTML,
+ serializerConfig: {},
+ }).deserialize({
+ schema: tiptapEditor.schema,
+ content: BULLET_LIST_MARKDOWN,
+ });
+
+ const expected = doc(
+ bulletList(
+ { bullet: '+', source: '+ list item 1\n+ list item 2' },
+ listItem({ source: '+ list item 1' }, paragraph('list item 1')),
+ listItem(
+ { source: '+ list item 2' },
+ paragraph('list item 2'),
+ bulletList(
+ { bullet: '-', source: '- embedded list item 3' },
+ listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')),
+ ),
+ ),
+ ),
+ );
+
+ expect(deserialized).toEqual(expected.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index b5a2abc2389..cf5aa3f2938 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -98,9 +98,7 @@ export const createTestContentEditorExtension = ({ commands = [] } = {}) => {
return {
labelName: {
default: null,
- parseHTML: (element) => {
- return { labelName: element.dataset.labelName };
- },
+ parseHTML: (element) => element.dataset.labelName,
},
};
},
diff --git a/spec/frontend/cycle_analytics/banner_spec.js b/spec/frontend/cycle_analytics/banner_spec.js
deleted file mode 100644
index ef7998c5ff5..00000000000
--- a/spec/frontend/cycle_analytics/banner_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Banner from '~/cycle_analytics/components/banner.vue';
-
-describe('Value Stream Analytics banner', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMount(Banner, {
- propsData: {
- documentationLink: 'path',
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render value stream analytics information', () => {
- expect(wrapper.find('h4').text().trim()).toBe('Introducing Value Stream Analytics');
-
- expect(
- wrapper
- .find('p')
- .text()
- .trim()
- .replace(/[\r\n]+/g, ' '),
- ).toContain(
- 'Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project.',
- );
-
- expect(wrapper.find('a').text().trim()).toBe('Read more');
- expect(wrapper.find('a').attributes('href')).toBe('path');
- });
-
- it('should emit an event when close button is clicked', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- await wrapper.find('.js-ca-dismiss-button').trigger('click');
-
- expect(wrapper.vm.$emit).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
index 71830eed3ef..5d3361bfa35 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -6,6 +6,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BaseComponent from '~/cycle_analytics/components/base.vue';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
+import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
import initState from '~/cycle_analytics/store/state';
@@ -30,13 +31,14 @@ Vue.use(Vuex);
let wrapper;
+const { id: groupId, path: groupPath } = currentGroup;
const defaultState = {
permissions,
currentGroup,
createdBefore,
createdAfter,
stageCounts,
- endpoints: { fullPath },
+ endpoints: { fullPath, groupId, groupPath },
};
function createStore({ initialState = {}, initialGetters = {} }) {
@@ -74,6 +76,7 @@ function createComponent({ initialState, initialGetters } = {}) {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPathNavigation = () => wrapper.findComponent(PathNavigation);
+const findFilters = () => wrapper.findComponent(ValueStreamFilters);
const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics);
const findStageTable = () => wrapper.findComponent(StageTable);
const findStageEvents = () => findStageTable().props('stageEvents');
@@ -123,6 +126,29 @@ describe('Value stream analytics component', () => {
expect(findStageEvents()).toEqual(selectedStageEvents);
});
+ it('renders the filters', () => {
+ expect(findFilters().exists()).toBe(true);
+ });
+
+ it('displays the date range selector and hides the project selector', () => {
+ expect(findFilters().props()).toMatchObject({
+ hasProjectFilter: false,
+ hasDateRangeFilter: true,
+ });
+ });
+
+ it('passes the paths to the filter bar', () => {
+ expect(findFilters().props()).toEqual({
+ groupId,
+ groupPath,
+ endDate: createdBefore,
+ hasDateRangeFilter: true,
+ hasProjectFilter: false,
+ selectedProjects: [],
+ startDate: createdAfter,
+ });
+ });
+
it('does not render the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js
index 47a2ce4444b..3158446c37d 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/cycle_analytics/stage_table_spec.js
@@ -22,6 +22,7 @@ const findStageEvents = () => wrapper.findAllByTestId('vsa-stage-event');
const findPagination = () => wrapper.findByTestId('vsa-stage-pagination');
const findTable = () => wrapper.findComponent(GlTable);
const findTableHead = () => wrapper.find('thead');
+const findTableHeadColumns = () => findTableHead().findAll('th');
const findStageEventTitle = (ev) => extendedWrapper(ev).findByTestId('vsa-stage-event-title');
const findStageTime = () => wrapper.findByTestId('vsa-stage-event-time');
const findIcon = (name) => wrapper.findByTestId(`${name}-icon`);
@@ -244,6 +245,12 @@ describe('StageTable', () => {
wrapper.destroy();
});
+ it('can sort the table by each column', () => {
+ findTableHeadColumns().wrappers.forEach((w) => {
+ expect(w.attributes('aria-sort')).toBe('none');
+ });
+ });
+
it('clicking a table column will send tracking information', () => {
triggerTableSort();
@@ -275,5 +282,17 @@ describe('StageTable', () => {
},
]);
});
+
+ describe('with sortable=false', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ sortable: false });
+ });
+
+ it('cannot sort the table', () => {
+ findTableHeadColumns().wrappers.forEach((w) => {
+ expect(w.attributes('aria-sort')).toBeUndefined();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index 915a828ff19..97b5bd03e18 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -4,21 +4,41 @@ import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions';
import * as getters from '~/cycle_analytics/store/getters';
import httpStatusCodes from '~/lib/utils/http_status';
-import { allowedStages, selectedStage, selectedValueStream } from '../mock_data';
-
+import {
+ allowedStages,
+ selectedStage,
+ selectedValueStream,
+ currentGroup,
+ createdAfter,
+ createdBefore,
+} from '../mock_data';
+
+const { id: groupId, path: groupPath } = currentGroup;
+const mockMilestonesPath = 'mock-milestones.json';
+const mockLabelsPath = 'mock-labels.json';
const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
-const mockStartDate = 30;
-const mockEndpoints = { fullPath: mockFullPath, requestPath: mockRequestPath };
-const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' };
-
-const defaultState = { ...getters, selectedValueStream };
+const mockEndpoints = {
+ fullPath: mockFullPath,
+ requestPath: mockRequestPath,
+ labelsPath: mockLabelsPath,
+ milestonesPath: mockMilestonesPath,
+ groupId,
+ groupPath,
+};
+const mockSetDateActionCommit = {
+ payload: { createdAfter, createdBefore },
+ type: 'SET_DATE_RANGE',
+};
+
+const defaultState = { ...getters, selectedValueStream, createdAfter, createdBefore };
describe('Project Value Stream Analytics actions', () => {
let state;
let mock;
beforeEach(() => {
+ state = { ...defaultState };
mock = new MockAdapter(axios);
});
@@ -34,16 +54,17 @@ describe('Project Value Stream Analytics actions', () => {
{ type: 'fetchCycleAnalyticsData' },
{ type: 'fetchStageData' },
{ type: 'fetchStageMedians' },
+ { type: 'fetchStageCountValues' },
{ type: 'setLoading', payload: false },
];
describe.each`
- action | payload | expectedActions | expectedMutations
- ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
- ${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
- ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
- ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
- ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
+ action | payload | expectedActions | expectedMutations
+ ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
+ ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
+ ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
+ ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
+ ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () =>
@@ -60,6 +81,12 @@ describe('Project Value Stream Analytics actions', () => {
let mockDispatch;
let mockCommit;
const payload = { endpoints: mockEndpoints };
+ const mockFilterEndpoints = {
+ groupEndpoint: 'foo',
+ labelsEndpoint: mockLabelsPath,
+ milestonesEndpoint: mockMilestonesPath,
+ projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
+ };
beforeEach(() => {
mockDispatch = jest.fn(() => Promise.resolve());
@@ -76,6 +103,9 @@ describe('Project Value Stream Analytics actions', () => {
payload,
);
expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints });
+
+ expect(mockDispatch).toHaveBeenCalledTimes(4);
+ expect(mockDispatch).toHaveBeenCalledWith('filters/setEndpoints', mockFilterEndpoints);
expect(mockDispatch).toHaveBeenCalledWith('setLoading', true);
expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams');
expect(mockDispatch).toHaveBeenCalledWith('setLoading', false);
@@ -84,7 +114,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('fetchCycleAnalyticsData', () => {
beforeEach(() => {
- state = { endpoints: mockEndpoints };
+ state = { ...defaultState, endpoints: mockEndpoints };
mock = new MockAdapter(axios);
mock.onGet(mockRequestPath).reply(httpStatusCodes.OK);
});
@@ -129,7 +159,6 @@ describe('Project Value Stream Analytics actions', () => {
state = {
...defaultState,
endpoints: mockEndpoints,
- startDate: mockStartDate,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -152,7 +181,6 @@ describe('Project Value Stream Analytics actions', () => {
state = {
...defaultState,
endpoints: mockEndpoints,
- startDate: mockStartDate,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -177,7 +205,6 @@ describe('Project Value Stream Analytics actions', () => {
state = {
...defaultState,
endpoints: mockEndpoints,
- startDate: mockStartDate,
selectedStage,
};
mock = new MockAdapter(axios);
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 7fcfef98547..628e2a4e7ae 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -1,5 +1,4 @@
import { useFakeDate } from 'helpers/fake_date';
-import { DEFAULT_DAYS_TO_DISPLAY } from '~/cycle_analytics/constants';
import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations';
import {
@@ -65,15 +64,16 @@ describe('Project Value Stream Analytics mutations', () => {
expect(state).toMatchObject({ [stateKey]: value });
});
+ const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore };
const mockInitialPayload = {
endpoints: { requestPath: mockRequestPath },
currentGroup: { title: 'cool-group' },
id: 1337,
+ ...mockSetDatePayload,
};
const mockInitializedObj = {
endpoints: { requestPath: mockRequestPath },
- createdAfter: mockCreatedAfter,
- createdBefore: mockCreatedBefore,
+ ...mockSetDatePayload,
};
it.each`
@@ -89,9 +89,8 @@ describe('Project Value Stream Analytics mutations', () => {
it.each`
mutation | payload | stateKey | value
- ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY}
- ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter}
- ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore}
+ ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
+ ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
index 168ddcfeacc..403d0dce3fc 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -1,3 +1,4 @@
+import { GlModal } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
@@ -29,6 +30,8 @@ describe('Deploy freeze table', () => {
const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]');
const findEditDeployFreezeButton = () => wrapper.find('[data-testid="edit-deploy-freeze"]');
const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]');
+ const findDeleteDeployFreezeButton = () => wrapper.find('[data-testid="delete-deploy-freeze"]');
+ const findDeleteDeployFreezeModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
createComponent();
@@ -73,6 +76,29 @@ describe('Deploy freeze table', () => {
store.state.freezePeriods[0],
);
});
+
+ it('displays delete deploy freeze button', () => {
+ expect(findDeleteDeployFreezeButton().exists()).toBe(true);
+ });
+
+ it('confirms a user wants to delete a deploy freeze', async () => {
+ const [{ freezeStart, freezeEnd, cronTimezone }] = store.state.freezePeriods;
+ await findDeleteDeployFreezeButton().trigger('click');
+ const modal = findDeleteDeployFreezeModal();
+ expect(modal.text()).toContain(
+ `Deploy freeze from ${freezeStart} to ${freezeEnd} in ${cronTimezone.formattedTimezone} will be removed.`,
+ );
+ });
+
+ it('deletes the freeze period on confirmation', async () => {
+ await findDeleteDeployFreezeButton().trigger('click');
+ const modal = findDeleteDeployFreezeModal();
+ modal.vm.$emit('primary');
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'deleteFreezePeriod',
+ store.state.freezePeriods[0],
+ );
+ });
});
});
diff --git a/spec/frontend/deploy_freeze/helpers.js b/spec/frontend/deploy_freeze/helpers.js
index bfb84142662..598f14d45f6 100644
--- a/spec/frontend/deploy_freeze/helpers.js
+++ b/spec/frontend/deploy_freeze/helpers.js
@@ -1,7 +1,7 @@
import { secondsToHours } from '~/lib/utils/datetime_utility';
export const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json');
-export const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json');
+export const timezoneDataFixture = getJSONFixture('/timezones/short.json');
export const findTzByName = (identifier = '') =>
timezoneDataFixture.find(({ name }) => name.toLowerCase() === identifier.toLowerCase());
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index 6bc9c4d374c..ad67afdce75 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -5,6 +5,7 @@ import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
import createFlash from '~/flash';
+import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
@@ -12,6 +13,7 @@ jest.mock('~/api.js');
jest.mock('~/flash.js');
describe('deploy freeze store actions', () => {
+ const freezePeriodFixture = freezePeriodsFixture[0];
let mock;
let state;
@@ -24,6 +26,7 @@ describe('deploy freeze store actions', () => {
Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture });
Api.createFreezePeriod.mockResolvedValue();
Api.updateFreezePeriod.mockResolvedValue();
+ Api.deleteFreezePeriod.mockResolvedValue();
});
afterEach(() => {
@@ -195,4 +198,46 @@ describe('deploy freeze store actions', () => {
);
});
});
+
+ describe('deleteFreezePeriod', () => {
+ it('dispatch correct actions on deleting a freeze period', () => {
+ testAction(
+ actions.deleteFreezePeriod,
+ freezePeriodFixture,
+ state,
+ [
+ { type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
+ { type: 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS', payload: freezePeriodFixture.id },
+ ],
+ [],
+ () =>
+ expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(
+ state.projectId,
+ freezePeriodFixture.id,
+ ),
+ );
+ });
+
+ it('should show flash error and set error in state on delete failure', () => {
+ jest.spyOn(logger, 'logError').mockImplementation();
+ const error = new Error();
+ Api.deleteFreezePeriod.mockRejectedValue(error);
+
+ testAction(
+ actions.deleteFreezePeriod,
+ freezePeriodFixture,
+ state,
+ [
+ { type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
+ { type: 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR', payload: freezePeriodFixture.id },
+ ],
+ [],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+
+ expect(logger.logError).toHaveBeenCalledWith('Unable to delete deploy freeze', error);
+ },
+ );
+ });
+ });
});
diff --git a/spec/frontend/deploy_freeze/store/mutations_spec.js b/spec/frontend/deploy_freeze/store/mutations_spec.js
index f8683489340..878a755088c 100644
--- a/spec/frontend/deploy_freeze/store/mutations_spec.js
+++ b/spec/frontend/deploy_freeze/store/mutations_spec.js
@@ -28,9 +28,9 @@ describe('Deploy freeze mutations', () => {
describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => {
it('should set freeze periods and format timezones from identifiers to names', () => {
const timezoneNames = {
- 'Europe/Berlin': 'Berlin',
- 'Etc/UTC': 'UTC',
- 'America/New_York': 'Eastern Time (US & Canada)',
+ 'Europe/Berlin': '[UTC 2] Berlin',
+ 'Etc/UTC': '[UTC 0] UTC',
+ 'America/New_York': '[UTC -4] Eastern Time (US & Canada)',
};
mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture);
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 7858f88f8c3..4a6dee31cd5 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -323,7 +323,7 @@ describe('deprecatedJQueryDropdown', () => {
const li = dropdown.renderItem(item, null, 3);
const link = li.querySelector('a');
- expect(link).toHaveAttr('data-track-event', 'click_text');
+ expect(link).toHaveAttr('data-track-action', 'click_text');
expect(link).toHaveAttr('data-track-label', 'some_value_for_label');
expect(link).toHaveAttr('data-track-value', '3');
expect(link).toHaveAttr('data-track-property', 'suggestion-category');
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
index d9f5ba0bade..4dc8eaea174 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn gl-mr-3 gl-w-auto! btn-confirm btn-md disabled gl-button\\">
+"<button data-track-action=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn gl-mr-3 gl-w-auto! btn-confirm btn-md disabled gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">
Comment
@@ -9,7 +9,7 @@ exports[`Design reply form component renders button text as "Comment" when creat
`;
exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
-"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn gl-mr-3 gl-w-auto! btn-confirm btn-md disabled gl-button\\">
+"<button data-track-action=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn gl-mr-3 gl-w-auto! btn-confirm btn-md disabled gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">
Save comment
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index 8a123b2d1e5..095c070e5e8 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -13,7 +13,11 @@ describe('Design management design scaler component', () => {
const setScale = (scale) => wrapper.vm.setScale(scale);
const createComponent = () => {
- wrapper = shallowMount(DesignScaler);
+ wrapper = shallowMount(DesignScaler, {
+ propsData: {
+ maxScale: 2,
+ },
+ });
};
beforeEach(() => {
@@ -61,6 +65,18 @@ describe('Design management design scaler component', () => {
expect(wrapper.emitted('scale')).toEqual([[1.2]]);
});
+ it('computes & increments correct stepSize based on maxScale', async () => {
+ wrapper.setProps({ maxScale: 11 });
+
+ await wrapper.vm.$nextTick();
+
+ getIncreaseScaleButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted().scale[0][0]).toBe(3);
+ });
+
describe('when `scale` value is 1', () => {
it('disables the "reset" button', () => {
const resetButton = getResetScaleButton();
@@ -77,7 +93,7 @@ describe('Design management design scaler component', () => {
});
});
- describe('when `scale` value is 2 (maximum)', () => {
+ describe('when `scale` value is maximum', () => {
beforeEach(async () => {
setScale(2);
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index 637f22457c4..67e4a82787c 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -3,10 +3,14 @@
exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
<gl-dropdown-stub
category="primary"
+ clearalltext="Clear all"
headertext=""
hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
issueiid=""
projectpath=""
+ showhighlighteditemstitle="true"
size="small"
text="Showing latest version"
variant="default"
@@ -80,10 +84,14 @@ exports[`Design management design version dropdown component renders design vers
exports[`Design management design version dropdown component renders design version list 1`] = `
<gl-dropdown-stub
category="primary"
+ clearalltext="Clear all"
headertext=""
hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
issueiid=""
projectpath=""
+ showhighlighteditemstitle="true"
size="small"
text="Showing latest version"
variant="default"
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 57023c55878..3d04840b1f8 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
@@ -25,7 +25,9 @@ exports[`Design management design index page renders design index 1`] = `
<div
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
>
- <design-scaler-stub />
+ <design-scaler-stub
+ maxscale="2"
+ />
</div>
</div>
@@ -186,7 +188,9 @@ exports[`Design management design index page with error GlAlert is rendered in c
<div
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
>
- <design-scaler-stub />
+ <design-scaler-stub
+ maxscale="2"
+ />
</div>
</div>
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 1332e872246..6ce384b4869 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -390,28 +390,13 @@ describe('Design management design index page', () => {
);
});
- describe('with usage_data_design_action enabled', () => {
- it('tracks design view service ping', () => {
- createComponent(
- { loading: true },
- {
- provide: {
- glFeatures: { usageDataDesignAction: true },
- },
- },
- );
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
- DESIGN_SERVICE_PING_EVENT_TYPES.DESIGN_ACTION,
- );
- });
- });
+ it('tracks design view service ping', () => {
+ createComponent({ loading: true });
- describe('with usage_data_design_action disabled', () => {
- it("doesn't track design view service ping", () => {
- createComponent({ loading: true });
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(0);
- });
+ expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
+ expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ DESIGN_SERVICE_PING_EVENT_TYPES.DESIGN_ACTION,
+ );
});
});
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 95cb1ac943c..ce79feae2e7 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -338,6 +338,13 @@ describe('Design management index page', () => {
__typename: 'DesignVersion',
id: expect.anything(),
sha: expect.anything(),
+ createdAt: '',
+ author: {
+ __typename: 'UserCore',
+ id: expect.anything(),
+ name: '',
+ avatarUrl: '',
+ },
},
},
},
@@ -623,6 +630,16 @@ describe('Design management index page', () => {
expect(mockMutate).not.toHaveBeenCalled();
});
+ it('does not upload designs if designs wrapper is destroyed', () => {
+ findDesignsWrapper().trigger('mouseenter');
+
+ wrapper.destroy();
+
+ document.dispatchEvent(event);
+
+ expect(mockMutate).not.toHaveBeenCalled();
+ });
+
describe('when designs wrapper is hovered', () => {
let realDateNow;
const today = () => new Date('2020-12-25');
diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js
index 5b7f99e9d96..dc6056badb9 100644
--- a/spec/frontend/design_management/utils/design_management_utils_spec.js
+++ b/spec/frontend/design_management/utils/design_management_utils_spec.js
@@ -101,7 +101,13 @@ describe('optimistic responses', () => {
discussions: { __typename: 'DesignDiscussion', nodes: [] },
versions: {
__typename: 'DesignVersionConnection',
- nodes: { __typename: 'DesignVersion', id: -1, sha: -1 },
+ nodes: {
+ __typename: 'DesignVersion',
+ id: expect.anything(),
+ sha: expect.anything(),
+ createdAt: '',
+ author: { __typename: 'UserCore', avatarUrl: '', name: '', id: expect.anything() },
+ },
},
},
],
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 1464dd84666..9dc82bbdc93 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -183,7 +183,7 @@ describe('diffs/components/app', () => {
it('displays loading icon on batch loading', () => {
createComponent({}, ({ state }) => {
- state.diffs.isBatchLoading = true;
+ state.diffs.batchLoadingState = 'loading';
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
@@ -705,24 +705,4 @@ describe('diffs/components/app', () => {
);
});
});
-
- describe('diff file tree is aware of review bar', () => {
- it('it does not have review-bar-visible class when review bar is not visible', () => {
- createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
- });
-
- expect(wrapper.find('.js-diff-tree-list').exists()).toBe(true);
- expect(wrapper.find('.js-diff-tree-list.review-bar-visible').exists()).toBe(false);
- });
-
- it('it does have review-bar-visible class when review bar is visible', () => {
- createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
- state.batchComments.drafts = ['draft message'];
- });
-
- expect(wrapper.find('.js-diff-tree-list.review-bar-visible').exists()).toBe(true);
- });
- });
});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 3dec56f2fe3..feb7118744b 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -242,32 +242,20 @@ describe('DiffFile', () => {
});
it.each`
- loggedIn | featureOn | bool
- ${true} | ${true} | ${true}
- ${false} | ${true} | ${false}
- ${true} | ${false} | ${false}
- ${false} | ${false} | ${false}
- `(
- 'should be $bool when { userIsLoggedIn: $loggedIn, featureEnabled: $featureOn }',
- ({ loggedIn, featureOn, bool }) => {
- setLoggedIn(loggedIn);
-
- ({ wrapper } = createComponent({
- options: {
- provide: {
- glFeatures: {
- localFileReviews: featureOn,
- },
- },
- },
- props: {
- file: store.state.diffs.diffFiles[0],
- },
- }));
+ loggedIn | bool
+ ${true} | ${true}
+ ${false} | ${false}
+ `('should be $bool when { userIsLoggedIn: $loggedIn }', ({ loggedIn, bool }) => {
+ setLoggedIn(loggedIn);
+
+ ({ wrapper } = createComponent({
+ props: {
+ file: store.state.diffs.diffFiles[0],
+ },
+ }));
- expect(wrapper.vm.showLocalFileReviews).toBe(bool);
- },
- );
+ expect(wrapper.vm.showLocalFileReviews).toBe(bool);
+ });
});
});
diff --git a/spec/frontend/diffs/create_diffs_store.js b/spec/frontend/diffs/create_diffs_store.js
index e6a8b7a72ae..307ebdaa4ac 100644
--- a/spec/frontend/diffs/create_diffs_store.js
+++ b/spec/frontend/diffs/create_diffs_store.js
@@ -9,6 +9,12 @@ Vue.use(Vuex);
export default function createDiffsStore() {
return new Vuex.Store({
modules: {
+ page: {
+ namespaced: true,
+ state: {
+ activeTab: 'notes',
+ },
+ },
diffs: diffsModule(),
notes: notesModule(),
batchComments: batchCommentsModule(),
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 6d005b868a9..b35abc9da02 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -186,15 +186,16 @@ describe('DiffsStoreActions', () => {
{},
{ endpointBatch, diffViewType: 'inline' },
[
- { type: types.SET_BATCH_LOADING, payload: true },
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'loading' },
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
- { type: types.SET_BATCH_LOADING, payload: false },
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
{ type: types.VIEW_DIFF_FILE, payload: 'test' },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
- { type: types.SET_BATCH_LOADING, payload: false },
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
{ type: types.VIEW_DIFF_FILE, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
+ { type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
],
[{ type: 'startRenderDiffsQueue' }, { type: 'startRenderDiffsQueue' }],
done,
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index b549ca42634..fc9ba223d5a 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -31,13 +31,13 @@ describe('DiffsStoreMutations', () => {
});
});
- describe('SET_BATCH_LOADING', () => {
+ describe('SET_BATCH_LOADING_STATE', () => {
it('should set loading state', () => {
const state = {};
- mutations[types.SET_BATCH_LOADING](state, false);
+ mutations[types.SET_BATCH_LOADING_STATE](state, false);
- expect(state.isBatchLoading).toEqual(false);
+ expect(state.batchLoadingState).toEqual(false);
});
});
diff --git a/spec/frontend/diffs/utils/preferences_spec.js b/spec/frontend/diffs/utils/preferences_spec.js
deleted file mode 100644
index 2dcc71dc188..00000000000
--- a/spec/frontend/diffs/utils/preferences_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Cookies from 'js-cookie';
-import {
- DIFF_FILE_BY_FILE_COOKIE_NAME,
- DIFF_VIEW_FILE_BY_FILE,
- DIFF_VIEW_ALL_FILES,
-} from '~/diffs/constants';
-import { fileByFile } from '~/diffs/utils/preferences';
-
-describe('diffs preferences', () => {
- describe('fileByFile', () => {
- afterEach(() => {
- Cookies.remove(DIFF_FILE_BY_FILE_COOKIE_NAME);
- });
-
- it.each`
- result | preference | cookie
- ${true} | ${false} | ${DIFF_VIEW_FILE_BY_FILE}
- ${false} | ${true} | ${DIFF_VIEW_ALL_FILES}
- ${true} | ${false} | ${DIFF_VIEW_FILE_BY_FILE}
- ${false} | ${true} | ${DIFF_VIEW_ALL_FILES}
- ${false} | ${false} | ${DIFF_VIEW_ALL_FILES}
- ${true} | ${true} | ${DIFF_VIEW_FILE_BY_FILE}
- `(
- 'should return $result when { preference: $preference, cookie: $cookie }',
- ({ result, preference, cookie }) => {
- Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, cookie);
-
- expect(fileByFile(preference)).toBe(result);
- },
- );
- });
-});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 5e6ccbd7cda..acf7d0780cd 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,9 +1,12 @@
+import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import mock from 'xhr-mock';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
import dropzoneInput from '~/dropzone_input';
+import axios from '~/lib/utils/axios_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
@@ -29,6 +32,16 @@ describe('dropzone_input', () => {
});
describe('handlePaste', () => {
+ const triggerPasteEvent = (clipboardData = {}) => {
+ const event = $.Event('paste');
+ const origEvent = new Event('paste');
+
+ origEvent.clipboardData = clipboardData;
+ event.originalEvent = origEvent;
+
+ $('.js-gfm-input').trigger(event);
+ };
+
beforeEach(() => {
loadFixtures('issues/new-issue.html');
@@ -38,24 +51,39 @@ describe('dropzone_input', () => {
});
it('pastes Markdown tables', () => {
- const event = $.Event('paste');
- const origEvent = new Event('paste');
+ jest.spyOn(PasteMarkdownTable.prototype, 'isTable');
+ jest.spyOn(PasteMarkdownTable.prototype, 'convertToTableMarkdown');
- origEvent.clipboardData = {
+ triggerPasteEvent({
types: ['text/plain', 'text/html'],
getData: () => '<table><tr><td>Hello World</td></tr></table>',
items: [],
- };
- event.originalEvent = origEvent;
-
- jest.spyOn(PasteMarkdownTable.prototype, 'isTable');
- jest.spyOn(PasteMarkdownTable.prototype, 'convertToTableMarkdown');
-
- $('.js-gfm-input').trigger(event);
+ });
expect(PasteMarkdownTable.prototype.isTable).toHaveBeenCalled();
expect(PasteMarkdownTable.prototype.convertToTableMarkdown).toHaveBeenCalled();
});
+
+ it('passes truncated long filename to post request', async () => {
+ const axiosMock = new MockAdapter(axios);
+ const longFileName = 'a'.repeat(300);
+
+ triggerPasteEvent({
+ types: ['text/plain', 'text/html', 'text/rtf', 'Files'],
+ getData: () => longFileName,
+ items: [
+ {
+ kind: 'file',
+ type: 'image/png',
+ getAsFile: () => new Blob(),
+ },
+ ],
+ });
+
+ axiosMock.onPost().reply(httpStatusCodes.OK, { link: { markdown: 'foo' } });
+ await waitForPromises();
+ expect(axiosMock.history.post[0].data.get('file').name).toHaveLength(246);
+ });
});
describe('shows error message', () => {
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index 1e6f5483160..9652c513671 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -9,6 +9,7 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
+import { sanitize } from '~/lib/dompurify';
const emptySupportMap = {
personZwj: false,
@@ -379,7 +380,7 @@ describe('emoji', () => {
describe('searchEmoji', () => {
const emojiFixture = Object.keys(mockEmojiData).reduce((acc, k) => {
const { name, e, u, d } = mockEmojiData[k];
- acc[k] = { name, e, u, d };
+ acc[k] = { name, e: sanitize(e), u, d };
return acc;
}, {});
@@ -397,6 +398,7 @@ describe('emoji', () => {
'heart',
'custard',
'star',
+ 'xss',
].map((name) => {
return {
emoji: emojiFixture[name],
@@ -620,4 +622,13 @@ describe('emoji', () => {
expect(sortEmoji(scoredItems)).toEqual(expected);
});
});
+
+ describe('sanitize emojis', () => {
+ it('should return sanitized emoji', () => {
+ expect(getEmojiInfo('xss')).toEqual({
+ ...mockEmojiData.xss,
+ e: '<img src="x">',
+ });
+ });
+ });
});
diff --git a/spec/frontend/emoji/support/unicode_support_map_spec.js b/spec/frontend/emoji/support/unicode_support_map_spec.js
index 945e804a9fa..37f74db30b5 100644
--- a/spec/frontend/emoji/support/unicode_support_map_spec.js
+++ b/spec/frontend/emoji/support/unicode_support_map_spec.js
@@ -8,14 +8,14 @@ describe('Unicode Support Map', () => {
const stringSupportMap = 'stringSupportMap';
beforeEach(() => {
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockImplementation(() => {});
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockImplementation(() => {});
jest.spyOn(JSON, 'parse').mockImplementation(() => {});
jest.spyOn(JSON, 'stringify').mockReturnValue(stringSupportMap);
});
describe('if isLocalStorageAvailable is `true`', () => {
beforeEach(() => {
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
getUnicodeSupportMap();
});
@@ -38,7 +38,7 @@ describe('Unicode Support Map', () => {
describe('if isLocalStorageAvailable is `false`', () => {
beforeEach(() => {
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(false);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
getUnicodeSupportMap();
});
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index 3e7f5dd5ff4..2c8c054ccbd 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -15,15 +15,12 @@ const DEFAULT_OPTS = {
projectEnvironmentsPath: '/projects/environments',
updateEnvironmentPath: '/proejcts/environments/1',
},
- propsData: { environment: { name: 'foo', externalUrl: 'https://foo.example.com' } },
+ propsData: { environment: { id: '0', name: 'foo', external_url: 'https://foo.example.com' } },
};
describe('~/environments/components/edit.vue', () => {
let wrapper;
let mock;
- let name;
- let url;
- let form;
const createWrapper = (opts = {}) =>
mountExtended(EditEnvironment, {
@@ -34,9 +31,6 @@ describe('~/environments/components/edit.vue', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createWrapper();
- name = wrapper.findByLabelText('Name');
- url = wrapper.findByLabelText('External URL');
- form = wrapper.findByRole('form', { name: 'Edit environment' });
});
afterEach(() => {
@@ -44,19 +38,22 @@ describe('~/environments/components/edit.vue', () => {
wrapper.destroy();
});
+ const findNameInput = () => wrapper.findByLabelText('Name');
+ const findExternalUrlInput = () => wrapper.findByLabelText('External URL');
+ const findForm = () => wrapper.findByRole('form', { name: 'Edit environment' });
+
const showsLoading = () => wrapper.find(GlLoadingIcon).exists();
const submitForm = async (expected, response) => {
mock
.onPut(DEFAULT_OPTS.provide.updateEnvironmentPath, {
- name: expected.name,
external_url: expected.url,
+ id: '0',
})
.reply(...response);
- await name.setValue(expected.name);
- await url.setValue(expected.url);
+ await findExternalUrlInput().setValue(expected.url);
- await form.trigger('submit');
+ await findForm().trigger('submit');
await waitForPromises();
};
@@ -65,18 +62,8 @@ describe('~/environments/components/edit.vue', () => {
expect(header.exists()).toBe(true);
});
- it.each`
- input | value
- ${() => name} | ${'test'}
- ${() => url} | ${'https://example.org'}
- `('it changes the value of the input to $value', async ({ input, value }) => {
- await input().setValue(value);
-
- expect(input().element.value).toBe(value);
- });
-
it('shows loader after form is submitted', async () => {
- const expected = { name: 'test', url: 'https://google.ca' };
+ const expected = { url: 'https://google.ca' };
expect(showsLoading()).toBe(false);
@@ -86,7 +73,7 @@ describe('~/environments/components/edit.vue', () => {
});
it('submits the updated environment on submit', async () => {
- const expected = { name: 'test', url: 'https://google.ca' };
+ const expected = { url: 'https://google.ca' };
await submitForm(expected, [200, { path: '/test' }]);
@@ -94,11 +81,24 @@ describe('~/environments/components/edit.vue', () => {
});
it('shows errors on error', async () => {
- const expected = { name: 'test', url: 'https://google.ca' };
+ const expected = { url: 'https://google.ca' };
- await submitForm(expected, [400, { message: ['name taken'] }]);
+ await submitForm(expected, [400, { message: ['uh oh!'] }]);
- expect(createFlash).toHaveBeenCalledWith({ message: 'name taken' });
+ expect(createFlash).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
});
+
+ it('renders a disabled "Name" field', () => {
+ const nameInput = findNameInput();
+
+ expect(nameInput.attributes().disabled).toBe('disabled');
+ expect(nameInput.element.value).toBe('foo');
+ });
+
+ it('renders an "External URL" field', () => {
+ const urlInput = findExternalUrlInput();
+
+ expect(urlInput.element.value).toBe('https://foo.example.com');
+ });
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index ed8fda71dab..f1af08bcf32 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -102,4 +102,52 @@ describe('~/environments/components/form.vue', () => {
wrapper = createWrapper({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+ describe('when a new environment is being created', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ environment: {
+ name: '',
+ externalUrl: '',
+ },
+ });
+ });
+
+ it('renders an enabled "Name" field', () => {
+ const nameInput = wrapper.findByLabelText('Name');
+
+ expect(nameInput.attributes().disabled).toBeUndefined();
+ expect(nameInput.element.value).toBe('');
+ });
+
+ it('renders an "External URL" field', () => {
+ const urlInput = wrapper.findByLabelText('External URL');
+
+ expect(urlInput.element.value).toBe('');
+ });
+ });
+
+ describe('when an existing environment is being edited', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ environment: {
+ id: 1,
+ name: 'test',
+ externalUrl: 'https://example.com',
+ },
+ });
+ });
+
+ it('renders a disabled "Name" field', () => {
+ const nameInput = wrapper.findByLabelText('Name');
+
+ expect(nameInput.attributes().disabled).toBe('disabled');
+ expect(nameInput.element.value).toBe('test');
+ });
+
+ it('renders an "External URL" field', () => {
+ const urlInput = wrapper.findByLabelText('External URL');
+
+ expect(urlInput.element.value).toBe('https://example.com');
+ });
+ });
});
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index a568a7d5396..b930259149f 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -31,7 +31,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environment,
- canReadEnvironment: true,
tableData,
},
});
@@ -135,7 +134,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environmentWithoutDeployable,
- canReadEnvironment: true,
tableData,
},
});
@@ -161,7 +159,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environmentWithoutUpcomingDeployment,
- canReadEnvironment: true,
tableData,
},
});
@@ -177,7 +174,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environment,
- canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
@@ -205,7 +201,6 @@ describe('Environment item', () => {
...environment,
auto_stop_at: futureDate,
},
- canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
@@ -241,7 +236,6 @@ describe('Environment item', () => {
...environment,
auto_stop_at: pastDate,
},
- canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
@@ -360,7 +354,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: folder,
- canReadEnvironment: true,
tableData,
},
});
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index 71426ee5170..1851163ac68 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -28,7 +28,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: [folder],
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -50,7 +49,6 @@ describe('Environment table', () => {
await factory({
propsData: {
environments: [mockItem],
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -78,7 +76,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -114,7 +111,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -151,7 +147,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: [mockItem],
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -179,7 +174,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
- canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
@@ -230,7 +224,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -296,7 +289,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -335,7 +327,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -364,7 +355,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
@@ -415,7 +405,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
- canReadEnvironment: true,
...eeOnlyProps,
},
});
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index dc176001943..cd05ecbfb53 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -1,4 +1,4 @@
-import { GlTabs, GlAlert } from '@gitlab/ui';
+import { GlTabs } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -7,9 +7,7 @@ import DeployBoard from '~/environments/components/deploy_board.vue';
import EmptyState from '~/environments/components/empty_state.vue';
import EnableReviewAppModal from '~/environments/components/enable_review_app_modal.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
-import { ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME } from '~/environments/constants';
import axios from '~/lib/utils/axios_utils';
-import { setCookie, getCookie, removeCookie } from '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import { environment, folder } from './mock_data';
@@ -20,7 +18,6 @@ describe('Environment', () => {
const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
- canReadEnvironment: true,
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
userCalloutsPath: '/callouts',
@@ -50,7 +47,6 @@ describe('Environment', () => {
const findNewEnvironmentButton = () => wrapper.findByTestId('new-environment');
const findEnvironmentsTabAvailable = () => wrapper.find('.js-environments-tab-available > a');
const findEnvironmentsTabStopped = () => wrapper.find('.js-environments-tab-stopped > a');
- const findSurveyAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -283,49 +279,4 @@ describe('Environment', () => {
expect(wrapper.findComponent(GlTabs).attributes('value')).toBe('1');
});
});
-
- describe('survey alert', () => {
- beforeEach(async () => {
- mockRequest(200, { environments: [] });
- await createWrapper(true);
- });
-
- afterEach(() => {
- removeCookie(ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME);
- });
-
- describe('when the user has not dismissed the alert', () => {
- it('shows the alert', () => {
- expect(findSurveyAlert().exists()).toBe(true);
- });
-
- describe('when the user dismisses the alert', () => {
- beforeEach(() => {
- findSurveyAlert().vm.$emit('dismiss');
- });
-
- it('hides the alert', () => {
- expect(findSurveyAlert().exists()).toBe(false);
- });
-
- it('persists the dismisal using a cookie', () => {
- const cookieValue = getCookie(ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME);
-
- expect(cookieValue).toBe('true');
- });
- });
- });
-
- describe('when the user has previously dismissed the alert', () => {
- beforeEach(async () => {
- setCookie(ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME, 'true');
-
- await createWrapper(true);
- });
-
- it('does not show the alert', () => {
- expect(findSurveyAlert().exists()).toBe(false);
- });
- });
- });
});
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 6334060c736..305e7385b43 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -44,7 +44,6 @@ describe('Environments detail header component', () => {
TimeAgo,
},
propsData: {
- canReadEnvironment: false,
canAdminEnvironment: false,
canUpdateEnvironment: false,
canStopEnvironment: false,
@@ -60,7 +59,7 @@ describe('Environments detail header component', () => {
describe('default state with minimal access', () => {
beforeEach(() => {
- createWrapper({ props: { environment: createEnvironment() } });
+ createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
});
it('displays the environment name', () => {
@@ -164,7 +163,6 @@ describe('Environments detail header component', () => {
createWrapper({
props: {
environment: createEnvironment({ hasTerminals: true, externalUrl }),
- canReadEnvironment: true,
},
});
});
@@ -178,8 +176,7 @@ describe('Environments detail header component', () => {
beforeEach(() => {
createWrapper({
props: {
- environment: createEnvironment(),
- canReadEnvironment: true,
+ environment: createEnvironment({ metricsUrl: 'my metrics url' }),
metricsPath,
},
});
@@ -195,7 +192,6 @@ describe('Environments detail header component', () => {
createWrapper({
props: {
environment: createEnvironment(),
- canReadEnvironment: true,
canAdminEnvironment: true,
canStopEnvironment: true,
canUpdateEnvironment: true,
diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js
index e4661d27872..72a7449f24e 100644
--- a/spec/frontend/environments/environments_folder_view_spec.js
+++ b/spec/frontend/environments/environments_folder_view_spec.js
@@ -11,7 +11,6 @@ describe('Environments Folder View', () => {
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
- canReadEnvironment: true,
cssContainerClass: 'container',
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index d02ed8688c6..9eb57b2682f 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -14,7 +14,6 @@ describe('Environments Folder View', () => {
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
- canReadEnvironment: true,
cssContainerClass: 'container',
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index e0be81b3899..30541ba68a5 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -1,6 +1,9 @@
+import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue';
import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
@@ -14,20 +17,31 @@ describe('error tracking settings app', () => {
let wrapper;
function mountComponent() {
- wrapper = shallowMount(ErrorTrackingSettings, {
- localVue,
- store, // Override the imported store
- propsData: {
- initialEnabled: 'true',
- initialApiHost: TEST_HOST,
- initialToken: 'someToken',
- initialProject: null,
- listProjectsEndpoint: TEST_HOST,
- operationsSettingsEndpoint: TEST_HOST,
- },
- });
+ wrapper = extendedWrapper(
+ shallowMount(ErrorTrackingSettings, {
+ localVue,
+ store, // Override the imported store
+ propsData: {
+ initialEnabled: 'true',
+ initialIntegrated: 'false',
+ initialApiHost: TEST_HOST,
+ initialToken: 'someToken',
+ initialProject: null,
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+ },
+ }),
+ );
}
+ const findBackendSettingsSection = () => wrapper.findByTestId('tracking-backend-settings');
+ const findBackendSettingsRadioGroup = () =>
+ findBackendSettingsSection().findComponent(GlFormRadioGroup);
+ const findBackendSettingsRadioButtons = () =>
+ findBackendSettingsRadioGroup().findAllComponents(GlFormRadio);
+ const findElementWithText = (wrappers, text) => wrappers.filter((item) => item.text() === text);
+ const findSentrySettings = () => wrapper.findByTestId('sentry-setting-form');
+
beforeEach(() => {
store = createStore();
@@ -62,4 +76,46 @@ describe('error tracking settings app', () => {
});
});
});
+
+ describe('tracking-backend settings', () => {
+ it('contains a form-group with the correct label', () => {
+ expect(findBackendSettingsSection().attributes('label')).toBe('Error tracking backend');
+ });
+
+ it('contains a radio group', () => {
+ expect(findBackendSettingsRadioGroup().exists()).toBe(true);
+ });
+
+ it('contains the correct radio buttons', () => {
+ expect(findBackendSettingsRadioButtons()).toHaveLength(2);
+
+ expect(findElementWithText(findBackendSettingsRadioButtons(), 'Sentry')).toHaveLength(1);
+ expect(findElementWithText(findBackendSettingsRadioButtons(), 'GitLab')).toHaveLength(1);
+ });
+
+ it('toggles the sentry-settings section when sentry is selected as a tracking-backend', async () => {
+ expect(findSentrySettings().exists()).toBe(true);
+
+ // set the "integrated" setting to "true"
+ findBackendSettingsRadioGroup().vm.$emit('change', true);
+
+ await nextTick();
+
+ expect(findSentrySettings().exists()).toBe(false);
+ });
+
+ it.each([true, false])(
+ 'calls the `updateIntegrated` action when the setting changes to `%s`',
+ (integrated) => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ expect(store.dispatch).toHaveBeenCalledTimes(0);
+
+ findBackendSettingsRadioGroup().vm.$emit('change', integrated);
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenCalledWith('updateIntegrated', integrated);
+ },
+ );
+ });
});
diff --git a/spec/frontend/error_tracking_settings/mock.js b/spec/frontend/error_tracking_settings/mock.js
index e64a6d1fe14..b2d7a912518 100644
--- a/spec/frontend/error_tracking_settings/mock.js
+++ b/spec/frontend/error_tracking_settings/mock.js
@@ -42,6 +42,7 @@ export const sampleBackendProject = {
export const sampleFrontendSettings = {
apiHost: 'apiHost',
enabled: false,
+ integrated: false,
token: 'token',
selectedProject: {
slug: normalizedProject.slug,
@@ -54,6 +55,7 @@ export const sampleFrontendSettings = {
export const transformedSettings = {
api_host: 'apiHost',
enabled: false,
+ integrated: false,
token: 'token',
project: {
slug: normalizedProject.slug,
@@ -71,6 +73,7 @@ export const defaultProps = {
export const initialEmptyState = {
apiHost: '',
enabled: false,
+ integrated: false,
project: null,
token: '',
listProjectsEndpoint: TEST_HOST,
@@ -80,6 +83,7 @@ export const initialEmptyState = {
export const initialPopulatedState = {
apiHost: 'apiHost',
enabled: true,
+ integrated: true,
project: JSON.stringify(projectList[0]),
token: 'token',
listProjectsEndpoint: TEST_HOST,
diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
index 281db7d9686..1b9be042dd4 100644
--- a/spec/frontend/error_tracking_settings/store/actions_spec.js
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -202,5 +202,11 @@ describe('error tracking settings actions', () => {
done,
);
});
+
+ it.each([true, false])('should set the `integrated` flag to `%s`', async (payload) => {
+ await testAction(actions.updateIntegrated, payload, state, [
+ { type: types.UPDATE_INTEGRATED, payload },
+ ]);
+ });
});
});
diff --git a/spec/frontend/error_tracking_settings/store/mutation_spec.js b/spec/frontend/error_tracking_settings/store/mutation_spec.js
index 78fd56904b3..ecf1c91c08a 100644
--- a/spec/frontend/error_tracking_settings/store/mutation_spec.js
+++ b/spec/frontend/error_tracking_settings/store/mutation_spec.js
@@ -25,6 +25,7 @@ describe('error tracking settings mutations', () => {
expect(state.apiHost).toEqual('');
expect(state.enabled).toEqual(false);
+ expect(state.integrated).toEqual(false);
expect(state.selectedProject).toEqual(null);
expect(state.token).toEqual('');
expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
@@ -38,6 +39,7 @@ describe('error tracking settings mutations', () => {
expect(state.apiHost).toEqual('apiHost');
expect(state.enabled).toEqual(true);
+ expect(state.integrated).toEqual(true);
expect(state.selectedProject).toEqual(projectList[0]);
expect(state.token).toEqual('token');
expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
@@ -78,5 +80,11 @@ describe('error tracking settings mutations', () => {
expect(state.connectSuccessful).toBe(false);
expect(state.connectError).toBe(false);
});
+
+ it.each([true, false])('should update `integrated` to `%s`', (integrated) => {
+ mutations[types.UPDATE_INTEGRATED](state, integrated);
+
+ expect(state.integrated).toBe(integrated);
+ });
});
});
diff --git a/spec/frontend/error_tracking_settings/utils_spec.js b/spec/frontend/error_tracking_settings/utils_spec.js
index 4b144f7daf1..61e75cdc45e 100644
--- a/spec/frontend/error_tracking_settings/utils_spec.js
+++ b/spec/frontend/error_tracking_settings/utils_spec.js
@@ -11,12 +11,14 @@ describe('error tracking settings utils', () => {
const emptyFrontendSettingsObject = {
apiHost: '',
enabled: false,
+ integrated: false,
token: '',
selectedProject: null,
};
const transformedEmptySettingsObject = {
api_host: null,
enabled: false,
+ integrated: false,
token: null,
project: null,
};
diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js
index 2ba8c65a252..999bed1ffbd 100644
--- a/spec/frontend/experimentation/utils_spec.js
+++ b/spec/frontend/experimentation/utils_spec.js
@@ -37,6 +37,50 @@ describe('experiment Utilities', () => {
});
});
+ describe('getAllExperimentContexts', () => {
+ const schema = TRACKING_CONTEXT_SCHEMA;
+ let origGon;
+
+ beforeEach(() => {
+ origGon = window.gon;
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ });
+
+ it('collects all of the experiment contexts into a single array', () => {
+ const experiments = [
+ { experiment: 'abc', variant: 'candidate' },
+ { experiment: 'def', variant: 'control' },
+ { experiment: 'ghi', variant: 'blue' },
+ ];
+ window.gon = {
+ experiment: experiments.reduce((collector, { experiment, variant }) => {
+ return { ...collector, [experiment]: { experiment, variant } };
+ }, {}),
+ };
+
+ expect(experimentUtils.getAllExperimentContexts()).toEqual(
+ experiments.map((data) => ({ schema, data })),
+ );
+ });
+
+ it('returns an empty array if there are no experiments', () => {
+ window.gon.experiment = {};
+
+ expect(experimentUtils.getAllExperimentContexts()).toEqual([]);
+ });
+
+ it('includes all additional experiment data', () => {
+ const experiment = 'experimentWithCustomData';
+ const data = { experiment, variant: 'control', color: 'blue', style: 'rounded' };
+ window.gon.experiment[experiment] = data;
+
+ expect(experimentUtils.getAllExperimentContexts()).toContainEqual({ schema, data });
+ });
+ });
+
describe('isExperimentVariant', () => {
describe.each`
gon | input | output
diff --git a/spec/frontend/filtered_search/services/recent_searches_service_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
index 6711ce03d40..dfa53652eb1 100644
--- a/spec/frontend/filtered_search/services/recent_searches_service_spec.js
+++ b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
@@ -145,13 +145,13 @@ describe('RecentSearchesService', () => {
let isAvailable;
beforeEach(() => {
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe');
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage');
isAvailable = RecentSearchesService.isAvailable();
});
- it('should call .isLocalStorageAccessSafe', () => {
- expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ it('should call .canUseLocalStorage', () => {
+ expect(AccessorUtilities.canUseLocalStorage).toHaveBeenCalled();
});
it('should return a boolean', () => {
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index b581aac6aee..1edb8cb3f41 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -12,14 +12,71 @@
markdown: |-
* {-deleted-}
* {+added+}
-- name: subscript
- markdown: H<sub>2</sub>O
-- name: superscript
- markdown: 2<sup>8</sup> = 256
- name: strike
markdown: '~~del~~'
- name: horizontal_rule
markdown: '---'
+- name: html_marks
+ markdown: |-
+ * Content editor is ~~great~~<ins>amazing</ins>.
+ * If the changes <abbr title="Looks good to merge">LGTM</abbr>, please <abbr title="Merge when pipeline succeeds">MWPS</abbr>.
+ * The English song <q>Oh I do like to be beside the seaside</q> looks like this in Hebrew: <span dir="rtl">אה, אני אוהב להיות ליד חוף הים</span>. In the computer's memory, this is stored as <bdo dir="ltr">אה, אני אוהב להיות ליד חוף הים</bdo>.
+ * <cite>The Scream</cite> by Edvard Munch. Painted in 1893.
+ * <dfn>HTML</dfn> is the standard markup language for creating web pages.
+ * Do not forget to buy <mark>milk</mark> today.
+ * This is a paragraph and <small>smaller text goes here</small>.
+ * The concert starts at <time datetime="20:00">20:00</time> and you'll be able to enjoy the band for at least <time datetime="PT2H30M">2h 30m</time>.
+ * Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text (Windows).
+ * WWF's goal is to: <q>Build a future where people live in harmony with nature.</q> We hope they succeed.
+ * The error occured was: <samp>Keyboard not found. Press F1 to continue.</samp>
+ * The area of a triangle is: 1/2 x <var>b</var> x <var>h</var>, where <var>b</var> is the base, and <var>h</var> is the vertical height.
+ * <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
+ * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
+ * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
+- name: div
+ markdown: |-
+ <div>plain text</div>
+ <div>
+
+ just a plain ol' div, not much to _expect_!
+
+ </div>
+- name: figure
+ markdown: |-
+ <figure>
+
+ ![Elephant at sunset](elephant-sunset.jpg)
+
+ <figcaption>An elephant at sunset</figcaption>
+ </figure>
+ <figure>
+
+ ![A crocodile wearing crocs](croc-crocs.jpg)
+
+ <figcaption>
+
+ A crocodile wearing _crocs_!
+
+ </figcaption>
+ </figure>
+- name: description_list
+ markdown: |-
+ <dl>
+ <dt>Frog</dt>
+ <dd>Wet green thing</dd>
+ <dt>Rabbit</dt>
+ <dd>Warm fluffy thing</dd>
+ <dt>Punt</dt>
+ <dd>Kick a ball</dd>
+ <dd>Take a bet</dd>
+ <dt>Color</dt>
+ <dt>Colour</dt>
+ <dd>
+
+ Any hue except _white_ or **black**
+
+ </dd>
+ </dl>
- name: link
markdown: '[GitLab](https://gitlab.com)'
- name: attachment_link
@@ -66,16 +123,31 @@
- name: thematic_break
markdown: |-
---
-- name: bullet_list
+- name: bullet_list_style_1
markdown: |-
* list item 1
* list item 2
* embedded list item 3
+- name: bullet_list_style_2
+ markdown: |-
+ - list item 1
+ - list item 2
+ * embedded list item 3
+- name: bullet_list_style_3
+ markdown: |-
+ + list item 1
+ + list item 2
+ - embedded list item 3
- name: ordered_list
markdown: |-
1. list item 1
2. list item 2
3. list item 3
+- name: ordered_list_with_start_order
+ markdown: |-
+ 134. list item 1
+ 135. list item 2
+ 136. list item 3
- name: task_list
markdown: |-
* [x] hello
@@ -92,6 +164,11 @@
1. [ ] of nested
1. [x] task list
2. [ ] items
+- name: ordered_task_list_with_order
+ markdown: |-
+ 4893. [x] hello
+ 4894. [x] world
+ 4895. [ ] example
- name: image
markdown: '![alt text](https://gitlab.com/logo.png)'
- name: hard_break
@@ -102,17 +179,28 @@
markdown: |-
| header | header |
|--------|--------|
- | cell | cell |
- | cell | cell |
-- name: table_with_alignment
- markdown: |-
- | header | : header : | header : |
- |--------|------------|----------|
- | cell | cell | cell |
- | cell | cell | cell |
+ | `code` | cell with **bold** |
+ | ~~strike~~ | cell with _italic_ |
+
+ # content after table
- name: emoji
markdown: ':sparkles: :heart: :100:'
- name: reference
context: project_wiki
markdown: |-
Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
+- name: audio
+ markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
+- name: video
+ markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)'
+- name: audio_and_video_in_lists
+ markdown: |-
+ * ![Sample Audio](https://gitlab.com/1.mp3)
+ * ![Sample Video](https://gitlab.com/2.mp4)
+
+ 1. ![Sample Video](https://gitlab.com/1.mp4)
+ 2. ![Sample Audio](https://gitlab.com/2.mp3)
+
+ * [x] ![Sample Audio](https://gitlab.com/1.mp3)
+ * [x] ![Sample Audio](https://gitlab.com/2.mp3)
+ * [x] ![Sample Video](https://gitlab.com/3.mp4)
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
index 09e4f969e1d..42762fa56f9 100644
--- a/spec/frontend/fixtures/freeze_period.rb
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -39,13 +39,4 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
expect(response).to be_successful
end
end
-
- describe TimeZoneHelper, '(JavaScript fixtures)' do
- let(:response) { timezone_data.to_json }
-
- it 'api/freeze-periods/timezone_data.json' do
- # Looks empty but does things
- # More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38525/diffs#note_391048415
- end
- end
end
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index e29a58f43b9..d5d6f534def 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
+ let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
query_path = 'runner/graphql/'
@@ -27,14 +28,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project)
end
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
- end
-
describe GraphQL::Query, type: :request do
get_runners_query_name = 'get_runners.query.graphql'
+ before do
+ sign_in(admin)
+ enable_admin_mode!(admin)
+ end
+
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
end
@@ -55,6 +56,11 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
describe GraphQL::Query, type: :request do
get_runner_query_name = 'get_runner.query.graphql'
+ before do
+ sign_in(admin)
+ enable_admin_mode!(admin)
+ end
+
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
end
@@ -67,4 +73,35 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
end
+
+ describe GraphQL::Query, type: :request do
+ get_group_runners_query_name = 'get_group_runners.query.graphql'
+
+ let_it_be(:group_owner) { create(:user) }
+
+ before do
+ group.add_owner(group_owner)
+ end
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path,
+ first: 1
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
index be2ead756cf..1bd99f5cd7f 100644
--- a/spec/frontend/fixtures/startup_css.rb
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -40,6 +40,21 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
expect(response).to be_successful
end
+
+ # This Feature Flag is off by default
+ # This ensures that the correct css is generated
+ # When the feature flag is off, the general startup will capture it
+ # This will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/339348
+ it "startup_css/project-#{type}-search-ff-on.html" do
+ stub_feature_flags(new_header_search: true)
+
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ id: project
+ }
+
+ expect(response).to be_successful
+ end
end
describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
diff --git a/spec/frontend/fixtures/static/pipeline_graph.html b/spec/frontend/fixtures/static/pipeline_graph.html
deleted file mode 100644
index d2c30ff9211..00000000000
--- a/spec/frontend/fixtures/static/pipeline_graph.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<div class="pipeline-visualization js-pipeline-graph">
-<ul class="stage-column-list">
-<li class="stage-column">
-<div class="stage-name">
-<a href="/">
-Test
-<div class="builds-container">
-<ul>
-<li class="build">
-<div class="curve"></div>
-<a>
-<svg></svg>
-<div>
-stop_review
-</div>
-</a>
-</li>
-</ul>
-</div>
-</a>
-</div>
-</li>
-</ul>
-</div>
diff --git a/spec/frontend/fixtures/timezones.rb b/spec/frontend/fixtures/timezones.rb
new file mode 100644
index 00000000000..261dcf5e116
--- /dev/null
+++ b/spec/frontend/fixtures/timezones.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do
+ include JavaScriptFixturesHelpers
+ include TimeZoneHelper
+
+ let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json }
+
+ before(:all) do
+ clean_frontend_fixtures('timezones/')
+ end
+
+ it 'timezones/short.json' do
+ @timezones = timezone_data(format: :short)
+ end
+
+ it 'timezones/full.json' do
+ @timezones = timezone_data(format: :full)
+ end
+end
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index dacfc7ce707..fb0321545c2 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -109,7 +109,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
});
it('should dispatch `receiveFrequentItemsError`', (done) => {
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(false);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
mockedState.namespace = mockNamespace;
mockedState.storageKey = mockStorageKey;
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index da0ff2a64ec..bc8c6460cf4 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -182,7 +182,12 @@ describe('AppComponent', () => {
jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
- const fetchPagePromise = vm.fetchPage(2, null, null, true);
+ const fetchPagePromise = vm.fetchPage({
+ page: 2,
+ filterGroupsBy: null,
+ sortBy: null,
+ archived: true,
+ });
expect(vm.isLoading).toBe(true);
expect(vm.fetchGroups).toHaveBeenCalledWith({
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index dc1a10639fc..0ec1ef5a49e 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -41,13 +41,12 @@ describe('GroupsComponent', () => {
vm.change(2);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'fetchPage',
- 2,
- expect.any(Object),
- expect.any(Object),
- expect.any(Object),
- );
+ expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', {
+ page: 2,
+ archived: null,
+ filterGroupsBy: null,
+ sortBy: null,
+ });
});
});
});
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index 0da2f84f2a1..c81edad499c 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -1,29 +1,29 @@
-import { GlBanner, GlButton } from '@gitlab/ui';
+import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
import eventHub from '~/invite_members/event_hub';
-import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/lib/utils/common_utils');
-const isDismissedKey = 'invite_99_1';
const title = 'Collaborate with your team';
const body =
"We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge";
-const svgPath = '/illustrations/background';
-const inviteMembersPath = 'groups/members';
const buttonText = 'Invite your colleagues';
-const trackLabel = 'invite_members_banner';
+const provide = {
+ svgPath: '/illustrations/background',
+ inviteMembersPath: 'groups/members',
+ trackLabel: 'invite_members_banner',
+ calloutsPath: 'call/out/path',
+ calloutsFeatureId: 'some-feature-id',
+ groupId: '1',
+};
const createComponent = (stubs = {}) => {
return shallowMount(InviteMembersBanner, {
- provide: {
- svgPath,
- inviteMembersPath,
- isDismissedKey,
- trackLabel,
- },
+ provide,
stubs,
});
};
@@ -31,8 +31,10 @@ const createComponent = (stubs = {}) => {
describe('InviteMembersBanner', () => {
let wrapper;
let trackingSpy;
+ let mockAxios;
beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
document.body.dataset.page = 'any:page';
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
});
@@ -40,22 +42,28 @@ describe('InviteMembersBanner', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ mockAxios.restore();
unmockTracking();
});
describe('tracking', () => {
+ const mockTrackingOnWrapper = () => {
+ unmockTracking();
+ trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
+ };
+
beforeEach(() => {
wrapper = createComponent({ GlBanner });
});
const trackCategory = undefined;
- const displayEvent = 'invite_members_banner_displayed';
const buttonClickEvent = 'invite_members_banner_button_clicked';
- const dismissEvent = 'invite_members_banner_dismissed';
it('sends the displayEvent when the banner is displayed', () => {
+ const displayEvent = 'invite_members_banner_displayed';
+
expect(trackingSpy).toHaveBeenCalledWith(trackCategory, displayEvent, {
- label: trackLabel,
+ label: provide.trackLabel,
});
});
@@ -74,16 +82,20 @@ describe('InviteMembersBanner', () => {
it('sends the buttonClickEvent with correct trackCategory and trackLabel', () => {
expect(trackingSpy).toHaveBeenCalledWith(trackCategory, buttonClickEvent, {
- label: trackLabel,
+ label: provide.trackLabel,
});
});
});
it('sends the dismissEvent when the banner is dismissed', () => {
+ mockTrackingOnWrapper();
+ mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ const dismissEvent = 'invite_members_banner_dismissed';
+
wrapper.find(GlBanner).vm.$emit('close');
expect(trackingSpy).toHaveBeenCalledWith(trackCategory, dismissEvent, {
- label: trackLabel,
+ label: provide.trackLabel,
});
});
});
@@ -98,7 +110,7 @@ describe('InviteMembersBanner', () => {
});
it('uses the svgPath for the banner svgpath', () => {
- expect(findBanner().attributes('svgpath')).toBe(svgPath);
+ expect(findBanner().attributes('svgpath')).toBe(provide.svgPath);
});
it('uses the title from options for title', () => {
@@ -115,35 +127,20 @@ describe('InviteMembersBanner', () => {
});
describe('dismissing', () => {
- const findButton = () => wrapper.findAll(GlButton).at(1);
-
beforeEach(() => {
wrapper = createComponent({ GlBanner });
-
- findButton().vm.$emit('click');
});
- it('sets iDismissed to true', () => {
- expect(wrapper.vm.isDismissed).toBe(true);
+ it('should render the banner when not dismissed', () => {
+ expect(wrapper.find(GlBanner).exists()).toBe(true);
});
- it('sets the cookie with the isDismissedKey', () => {
- expect(setCookie).toHaveBeenCalledWith(isDismissedKey, true);
- });
- });
-
- describe('when a dismiss cookie exists', () => {
- beforeEach(() => {
- parseBoolean.mockReturnValue(true);
-
- wrapper = createComponent({ GlBanner });
- });
-
- it('sets isDismissed to true', () => {
- expect(wrapper.vm.isDismissed).toBe(true);
- });
+ it('should close the banner when dismiss is clicked', async () => {
+ mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ expect(wrapper.find(GlBanner).exists()).toBe(true);
+ wrapper.find(GlBanner).vm.$emit('close');
- it('does not render the banner', () => {
+ await wrapper.vm.$nextTick();
expect(wrapper.find(GlBanner).exists()).toBe(false);
});
});
diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js
index f350012ebed..49f3f5da43c 100644
--- a/spec/frontend/groups/components/item_stats_spec.js
+++ b/spec/frontend/groups/components/item_stats_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ItemStats from '~/groups/components/item_stats.vue';
import ItemStatsValue from '~/groups/components/item_stats_value.vue';
@@ -12,7 +12,7 @@ describe('ItemStats', () => {
};
const createComponent = (props = {}) => {
- wrapper = shallowMount(ItemStats, {
+ wrapper = shallowMountExtended(ItemStats, {
propsData: { ...defaultProps, ...props },
});
};
@@ -46,5 +46,31 @@ describe('ItemStats', () => {
expect(findItemStatsValue().props('cssClass')).toBe('project-stars');
expect(wrapper.find('.last-updated').exists()).toBe(true);
});
+
+ describe('group specific rendering', () => {
+ describe.each`
+ provided | state | data
+ ${true} | ${'displays'} | ${null}
+ ${false} | ${'does not display'} | ${{ subgroupCount: undefined, projectCount: undefined }}
+ `('when provided = $provided', ({ provided, state, data }) => {
+ beforeEach(() => {
+ const item = {
+ ...mockParentGroupItem,
+ ...data,
+ type: ITEM_TYPE.GROUP,
+ };
+
+ createComponent({ item });
+ });
+
+ it.each`
+ entity | testId
+ ${'subgroups'} | ${'subgroups-count'}
+ ${'projects'} | ${'projects-count'}
+ `(`${state} $entity count`, ({ testId }) => {
+ expect(wrapper.findByTestId(testId).exists()).toBe(provided);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
new file mode 100644
index 00000000000..2cbcb73ce5b
--- /dev/null
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -0,0 +1,159 @@
+import { GlSearchBoxByType } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import HeaderSearchApp from '~/header_search/components/app.vue';
+import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue';
+import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
+import { ENTER_KEY, ESC_KEY } from '~/lib/utils/keys';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { MOCK_SEARCH, MOCK_SEARCH_QUERY, MOCK_USERNAME } from '../mock_data';
+
+Vue.use(Vuex);
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
+
+describe('HeaderSearchApp', () => {
+ let wrapper;
+
+ const actionSpies = {
+ setSearch: jest.fn(),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ ...initialState,
+ },
+ actions: actionSpies,
+ getters: {
+ searchQuery: () => MOCK_SEARCH_QUERY,
+ },
+ });
+
+ wrapper = shallowMountExtended(HeaderSearchApp, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu');
+ const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems);
+ const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems);
+
+ describe('template', () => {
+ it('always renders Header Search Input', () => {
+ createComponent();
+ expect(findHeaderSearchInput().exists()).toBe(true);
+ });
+
+ describe.each`
+ showDropdown | username | showSearchDropdown
+ ${false} | ${null} | ${false}
+ ${false} | ${MOCK_USERNAME} | ${false}
+ ${true} | ${null} | ${false}
+ ${true} | ${MOCK_USERNAME} | ${true}
+ `('Header Search Dropdown', ({ showDropdown, username, showSearchDropdown }) => {
+ describe(`when showDropdown is ${showDropdown} and current_username is ${username}`, () => {
+ beforeEach(() => {
+ createComponent();
+ window.gon.current_username = username;
+ wrapper.setData({ showDropdown });
+ });
+
+ it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(showSearchDropdown);
+ });
+ });
+ });
+
+ describe.each`
+ search | showDefault | showScoped
+ ${null} | ${true} | ${false}
+ ${''} | ${true} | ${false}
+ ${MOCK_SEARCH} | ${false} | ${true}
+ `('Header Search Dropdown Items', ({ search, showDefault, showScoped }) => {
+ describe(`when search is ${search}`, () => {
+ beforeEach(() => {
+ createComponent({ search });
+ window.gon.current_username = MOCK_USERNAME;
+ wrapper.setData({ showDropdown: true });
+ });
+
+ it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => {
+ expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault);
+ });
+
+ it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => {
+ expect(findHeaderSearchScopedItems().exists()).toBe(showScoped);
+ });
+ });
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+ window.gon.current_username = MOCK_USERNAME;
+ });
+
+ describe('Header Search Input', () => {
+ describe('when dropdown is closed', () => {
+ it('onFocus opens dropdown', async () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ findHeaderSearchInput().vm.$emit('focus');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findHeaderSearchDropdown().exists()).toBe(true);
+ });
+
+ it('onClick opens dropdown', async () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ findHeaderSearchInput().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findHeaderSearchDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when dropdown is opened', () => {
+ beforeEach(() => {
+ wrapper.setData({ showDropdown: true });
+ });
+
+ it('onKey-Escape closes dropdown', async () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(true);
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ESC_KEY }));
+
+ await wrapper.vm.$nextTick();
+
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ });
+ });
+
+ it('calls setSearch when search input event is fired', async () => {
+ findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH);
+
+ await wrapper.vm.$nextTick();
+
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH);
+ });
+
+ it('submits a search onKey-Enter', async () => {
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ await wrapper.vm.$nextTick();
+
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/header_search/components/header_search_default_items_spec.js b/spec/frontend/header_search/components/header_search_default_items_spec.js
new file mode 100644
index 00000000000..ce083d0df72
--- /dev/null
+++ b/spec/frontend/header_search/components/header_search_default_items_spec.js
@@ -0,0 +1,81 @@
+import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import HeaderSearchDefaultItems from '~/header_search/components/header_search_default_items.vue';
+import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('HeaderSearchDefaultItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ searchContext: MOCK_SEARCH_CONTEXT,
+ ...initialState,
+ },
+ getters: {
+ defaultSearchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ },
+ });
+
+ wrapper = shallowMount(HeaderSearchDefaultItems, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
+ const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+
+ describe('template', () => {
+ describe('Dropdown items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item for each option in defaultSearchOptions', () => {
+ expect(findDropdownItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length);
+ });
+
+ it('renders titles correctly', () => {
+ const expectedTitles = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.title);
+ expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.url);
+ expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ });
+ });
+
+ describe.each`
+ group | project | dropdownTitle
+ ${null} | ${null} | ${'All GitLab'}
+ ${{ name: 'Test Group' }} | ${null} | ${'Test Group'}
+ ${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'}
+ `('Dropdown Header', ({ group, project, dropdownTitle }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createComponent({
+ searchContext: {
+ group,
+ project,
+ },
+ });
+ });
+
+ it(`should render as ${dropdownTitle}`, () => {
+ expect(findDropdownHeader().text()).toBe(dropdownTitle);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
new file mode 100644
index 00000000000..f0e5e182ec4
--- /dev/null
+++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
@@ -0,0 +1,61 @@
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { trimText } from 'helpers/text_helper';
+import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
+import { MOCK_SEARCH, MOCK_SCOPED_SEARCH_OPTIONS } from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('HeaderSearchScopedItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ search: MOCK_SEARCH,
+ ...initialState,
+ },
+ getters: {
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ },
+ });
+
+ wrapper = shallowMount(HeaderSearchScopedItems, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
+ const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+
+ describe('template', () => {
+ describe('Dropdown items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item for each option in scopedSearchOptions', () => {
+ expect(findDropdownItems()).toHaveLength(MOCK_SCOPED_SEARCH_OPTIONS.length);
+ });
+
+ it('renders titles correctly', () => {
+ const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
+ trimText(`"${MOCK_SEARCH}" ${o.description} ${o.scope || ''}`),
+ );
+ expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url);
+ expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js
new file mode 100644
index 00000000000..5963ad9c279
--- /dev/null
+++ b/spec/frontend/header_search/mock_data.js
@@ -0,0 +1,83 @@
+import {
+ MSG_ISSUES_ASSIGNED_TO_ME,
+ MSG_ISSUES_IVE_CREATED,
+ MSG_MR_ASSIGNED_TO_ME,
+ MSG_MR_IM_REVIEWER,
+ MSG_MR_IVE_CREATED,
+ MSG_IN_PROJECT,
+ MSG_IN_GROUP,
+ MSG_IN_ALL_GITLAB,
+} from '~/header_search/constants';
+
+export const MOCK_USERNAME = 'anyone';
+
+export const MOCK_SEARCH_PATH = '/search';
+
+export const MOCK_ISSUE_PATH = '/dashboard/issues';
+
+export const MOCK_MR_PATH = '/dashboard/merge_requests';
+
+export const MOCK_ALL_PATH = '/';
+
+export const MOCK_PROJECT = {
+ id: 123,
+ name: 'MockProject',
+ path: '/mock-project',
+};
+
+export const MOCK_GROUP = {
+ id: 321,
+ name: 'MockGroup',
+ path: '/mock-group',
+};
+
+export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test';
+
+export const MOCK_SEARCH = 'test';
+
+export const MOCK_SEARCH_CONTEXT = {
+ project: null,
+ project_metadata: {},
+ group: null,
+ group_metadata: {},
+};
+
+export const MOCK_DEFAULT_SEARCH_OPTIONS = [
+ {
+ title: MSG_ISSUES_ASSIGNED_TO_ME,
+ url: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ },
+ {
+ title: MSG_ISSUES_IVE_CREATED,
+ url: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`,
+ },
+ {
+ title: MSG_MR_ASSIGNED_TO_ME,
+ url: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ },
+ {
+ title: MSG_MR_IM_REVIEWER,
+ url: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`,
+ },
+ {
+ title: MSG_MR_IVE_CREATED,
+ url: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`,
+ },
+];
+
+export const MOCK_SCOPED_SEARCH_OPTIONS = [
+ {
+ scope: MOCK_PROJECT.name,
+ description: MSG_IN_PROJECT,
+ url: MOCK_PROJECT.path,
+ },
+ {
+ scope: MOCK_GROUP.name,
+ description: MSG_IN_GROUP,
+ url: MOCK_GROUP.path,
+ },
+ {
+ description: MSG_IN_ALL_GITLAB,
+ url: MOCK_ALL_PATH,
+ },
+];
diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js
new file mode 100644
index 00000000000..4530df0d91c
--- /dev/null
+++ b/spec/frontend/header_search/store/actions_spec.js
@@ -0,0 +1,28 @@
+import testAction from 'helpers/vuex_action_helper';
+import * as actions from '~/header_search/store/actions';
+import * as types from '~/header_search/store/mutation_types';
+import createState from '~/header_search/store/state';
+import { MOCK_SEARCH } from '../mock_data';
+
+describe('Header Search Store Actions', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState({});
+ });
+
+ afterEach(() => {
+ state = null;
+ });
+
+ describe('setSearch', () => {
+ it('calls the SET_SEARCH mutation', () => {
+ return testAction({
+ action: actions.setSearch,
+ payload: MOCK_SEARCH,
+ state,
+ expectedMutations: [{ type: types.SET_SEARCH, payload: MOCK_SEARCH }],
+ });
+ });
+ });
+});
diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js
new file mode 100644
index 00000000000..2ad0a082f6a
--- /dev/null
+++ b/spec/frontend/header_search/store/getters_spec.js
@@ -0,0 +1,211 @@
+import * as getters from '~/header_search/store/getters';
+import initState from '~/header_search/store/state';
+import {
+ MOCK_USERNAME,
+ MOCK_SEARCH_PATH,
+ MOCK_ISSUE_PATH,
+ MOCK_MR_PATH,
+ MOCK_SEARCH_CONTEXT,
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_PROJECT,
+ MOCK_GROUP,
+ MOCK_ALL_PATH,
+ MOCK_SEARCH,
+} from '../mock_data';
+
+describe('Header Search Store Getters', () => {
+ let state;
+
+ const createState = (initialState) => {
+ state = initState({
+ searchPath: MOCK_SEARCH_PATH,
+ issuesPath: MOCK_ISSUE_PATH,
+ mrPath: MOCK_MR_PATH,
+ searchContext: MOCK_SEARCH_CONTEXT,
+ ...initialState,
+ });
+ };
+
+ afterEach(() => {
+ state = null;
+ });
+
+ describe.each`
+ group | project | expectedPath
+ ${null} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=undefined&group_id=undefined&scope=issues`}
+ ${MOCK_GROUP} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=undefined&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`}
+ `('searchQuery', ({ group, project, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope: 'issues',
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.searchQuery(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | group_metadata | project | project_metadata | expectedPath
+ ${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH}
+ ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${'group/path'}
+ ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ issues_path: 'project/path' }} | ${'project/path'}
+ `('scopedIssuesPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ group_metadata,
+ project,
+ project_metadata,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.scopedIssuesPath(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | group_metadata | project | project_metadata | expectedPath
+ ${null} | ${null} | ${null} | ${null} | ${MOCK_MR_PATH}
+ ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${null} | ${null} | ${'group/path'}
+ ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ mr_path: 'project/path' }} | ${'project/path'}
+ `('scopedMRPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ group_metadata,
+ project,
+ project_metadata,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.scopedMRPath(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | expectedPath
+ ${null} | ${null} | ${null}
+ ${MOCK_GROUP} | ${null} | ${null}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`}
+ `('projectUrl', ({ group, project, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope: 'issues',
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.projectUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | expectedPath
+ ${null} | ${null} | ${null}
+ ${MOCK_GROUP} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`}
+ `('groupUrl', ({ group, project, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope: 'issues',
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.groupUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe('allUrl', () => {
+ const expectedPath = `${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues`;
+
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ scope: 'issues',
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.allUrl(state)).toBe(expectedPath);
+ });
+ });
+
+ describe('defaultSearchOptions', () => {
+ const mockGetters = {
+ scopedIssuesPath: MOCK_ISSUE_PATH,
+ scopedMRPath: MOCK_MR_PATH,
+ };
+
+ beforeEach(() => {
+ createState();
+ window.gon.current_username = MOCK_USERNAME;
+ });
+
+ it('returns the correct array', () => {
+ expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ );
+ });
+ });
+
+ describe('scopedSearchOptions', () => {
+ const mockGetters = {
+ projectUrl: MOCK_PROJECT.path,
+ groupUrl: MOCK_GROUP.path,
+ allUrl: MOCK_ALL_PATH,
+ };
+
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ project: MOCK_PROJECT,
+ group: MOCK_GROUP,
+ },
+ });
+ });
+
+ it('returns the correct array', () => {
+ expect(getters.scopedSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/header_search/store/mutations_spec.js b/spec/frontend/header_search/store/mutations_spec.js
new file mode 100644
index 00000000000..8196c06099d
--- /dev/null
+++ b/spec/frontend/header_search/store/mutations_spec.js
@@ -0,0 +1,20 @@
+import * as types from '~/header_search/store/mutation_types';
+import mutations from '~/header_search/store/mutations';
+import createState from '~/header_search/store/state';
+import { MOCK_SEARCH } from '../mock_data';
+
+describe('Header Search Store Mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState({});
+ });
+
+ describe('SET_SEARCH', () => {
+ it('sets search to value', () => {
+ mutations[types.SET_SEARCH](state, MOCK_SEARCH);
+
+ expect(state.search).toBe(MOCK_SEARCH);
+ });
+ });
+});
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 4ca6d7259bd..0d43accb7e5 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -59,8 +59,8 @@ describe('Header', () => {
beforeEach(() => {
setFixtures(`
<li class="js-nav-user-dropdown">
- <a class="js-buy-pipeline-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy Pipeline minutes</a>
- <a class="js-upgrade-plan-link" data-track-event="click_upgrade_link" data-track-label="free" data-track-property="user_dropdown">Upgrade</a>
+ <a class="js-buy-pipeline-minutes-link" data-track-action="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy Pipeline minutes</a>
+ <a class="js-upgrade-plan-link" data-track-action="click_upgrade_link" data-track-label="free" data-track-property="user_dropdown">Upgrade</a>
</li>`);
trackingSpy = mockTracking('_category_', $('.js-nav-user-dropdown').element, jest.spyOn);
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 47bcfb59a5f..c2212eea849 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data';
import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants';
+import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
import SourceEditor from '~/editor/source_editor';
import RepoEditor from '~/ide/components/repo_editor.vue';
@@ -25,6 +26,7 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
import { file } from '../helpers';
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
+const CURRENT_PROJECT_ID = 'gitlab-org/gitlab';
const defaultFileProps = {
...file('file.txt'),
@@ -63,7 +65,7 @@ const prepareStore = (state, activeFile) => {
const localState = {
openFiles: [activeFile],
projects: {
- 'gitlab-org/gitlab': {
+ [CURRENT_PROJECT_ID]: {
branches: {
main: {
name: 'main',
@@ -74,7 +76,7 @@ const prepareStore = (state, activeFile) => {
},
},
},
- currentProjectId: 'gitlab-org/gitlab',
+ currentProjectId: CURRENT_PROJECT_ID,
currentBranchId: 'main',
entries: {
[activeFile.path]: activeFile,
@@ -98,6 +100,7 @@ describe('RepoEditor', () => {
let createInstanceSpy;
let createDiffInstanceSpy;
let createModelSpy;
+ let applyExtensionSpy;
const waitForEditorSetup = () =>
new Promise((resolve) => {
@@ -124,11 +127,28 @@ describe('RepoEditor', () => {
const findEditor = () => wrapper.find('[data-testid="editor-container"]');
const findTabs = () => wrapper.findAll('.ide-mode-tabs .nav-links li');
const findPreviewTab = () => wrapper.find('[data-testid="preview-tab"]');
+ const expectEditorMarkdownExtension = (shouldHaveExtension) => {
+ if (shouldHaveExtension) {
+ expect(applyExtensionSpy).toHaveBeenCalledWith(
+ wrapper.vm.editor,
+ expect.any(EditorMarkdownExtension),
+ );
+ // 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.previewMarkdownPath).toBe(PREVIEW_MARKDOWN_PATH);
+ } else {
+ expect(applyExtensionSpy).not.toHaveBeenCalledWith(
+ wrapper.vm.editor,
+ expect.any(EditorMarkdownExtension),
+ );
+ }
+ };
beforeEach(() => {
createInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_CODE_INSTANCE_FN);
createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN);
createModelSpy = jest.spyOn(monacoEditor, 'createModel');
+ applyExtensionSpy = jest.spyOn(SourceEditor, 'instanceApplyExtension');
jest.spyOn(service, 'getFileData').mockResolvedValue();
jest.spyOn(service, 'getRawFileData').mockResolvedValue();
});
@@ -280,13 +300,8 @@ describe('RepoEditor', () => {
'$prefix install markdown extension for $activeFile.name in $viewer viewer',
async ({ activeFile, viewer, shouldHaveMarkdownExtension } = {}) => {
await createComponent({ state: { viewer }, activeFile });
- if (shouldHaveMarkdownExtension) {
- expect(vm.editor.previewMarkdownPath).toBe(PREVIEW_MARKDOWN_PATH);
- expect(vm.editor.togglePreview).toBeDefined();
- } else {
- expect(vm.editor.previewMarkdownPath).toBeUndefined();
- expect(vm.editor.togglePreview).toBeUndefined();
- }
+
+ expectEditorMarkdownExtension(shouldHaveMarkdownExtension);
},
);
});
diff --git a/spec/frontend/ide/services/terminals_spec.js b/spec/frontend/ide/services/terminals_spec.js
new file mode 100644
index 00000000000..788fdb6471c
--- /dev/null
+++ b/spec/frontend/ide/services/terminals_spec.js
@@ -0,0 +1,51 @@
+import MockAdapter from 'axios-mock-adapter';
+import * as terminalService from '~/ide/services/terminals';
+import axios from '~/lib/utils/axios_utils';
+
+const TEST_PROJECT_PATH = 'lorem/ipsum/dolar';
+const TEST_BRANCH = 'ref';
+
+describe('~/ide/services/terminals', () => {
+ let axiosSpy;
+ let mock;
+ const prevRelativeUrlRoot = gon.relative_url_root;
+
+ beforeEach(() => {
+ axiosSpy = jest.fn().mockReturnValue([200, {}]);
+
+ mock = new MockAdapter(axios);
+ mock.onPost(/.*/).reply((...args) => axiosSpy(...args));
+ });
+
+ afterEach(() => {
+ gon.relative_url_root = prevRelativeUrlRoot;
+ mock.restore();
+ });
+
+ it.each`
+ method | relativeUrlRoot | url
+ ${'checkConfig'} | ${''} | ${`/${TEST_PROJECT_PATH}/ide_terminals/check_config`}
+ ${'checkConfig'} | ${'/'} | ${`/${TEST_PROJECT_PATH}/ide_terminals/check_config`}
+ ${'checkConfig'} | ${'/gitlabbin'} | ${`/gitlabbin/${TEST_PROJECT_PATH}/ide_terminals/check_config`}
+ ${'create'} | ${''} | ${`/${TEST_PROJECT_PATH}/ide_terminals`}
+ ${'create'} | ${'/'} | ${`/${TEST_PROJECT_PATH}/ide_terminals`}
+ ${'create'} | ${'/gitlabbin'} | ${`/gitlabbin/${TEST_PROJECT_PATH}/ide_terminals`}
+ `(
+ 'when $method called, posts request to $url (relative_url_root=$relativeUrlRoot)',
+ async ({ method, url, relativeUrlRoot }) => {
+ gon.relative_url_root = relativeUrlRoot;
+
+ await terminalService[method](TEST_PROJECT_PATH, TEST_BRANCH);
+
+ expect(axiosSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: JSON.stringify({
+ branch: TEST_BRANCH,
+ format: 'json',
+ }),
+ url,
+ }),
+ );
+ },
+ );
+});
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 00733615f81..2f8447af518 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -86,6 +86,14 @@ describe('WebIDE utils', () => {
expect(isTextFile({ name: 'abc.dat', content: '' })).toBe(true);
expect(isTextFile({ name: 'abc.dat', content: ' ' })).toBe(true);
});
+
+ it('returns true if there is a `binary` property already set on the file object', () => {
+ expect(isTextFile({ name: 'abc.txt', content: '' })).toBe(true);
+ expect(isTextFile({ name: 'abc.txt', content: '', binary: true })).toBe(false);
+
+ expect(isTextFile({ name: 'abc.tex', content: 'éêė' })).toBe(false);
+ expect(isTextFile({ name: 'abc.tex', content: 'éêė', binary: false })).toBe(true);
+ });
});
describe('trimPathComponents', () => {
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
new file mode 100644
index 00000000000..60f0780fdb3
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -0,0 +1,90 @@
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { STATUSES } from '~/import_entities/constants';
+import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
+import { generateFakeEntry } from '../graphql/fixtures';
+
+describe('import actions cell', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(ImportActionsCell, {
+ propsData: {
+ groupPathRegex: /^[a-zA-Z]+$/,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when import status is NONE', () => {
+ beforeEach(() => {
+ const group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ createComponent({ group });
+ });
+
+ it('renders import button', () => {
+ const button = wrapper.findComponent(GlButton);
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Import');
+ });
+
+ it('does not render icon with a hint', () => {
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(false);
+ });
+ });
+
+ describe('when import status is FINISHED', () => {
+ beforeEach(() => {
+ const group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED });
+ createComponent({ group });
+ });
+
+ it('renders re-import button', () => {
+ const button = wrapper.findComponent(GlButton);
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Re-import');
+ });
+
+ it('renders icon with a hint', () => {
+ const icon = wrapper.findComponent(GlIcon);
+ expect(icon.exists()).toBe(true);
+ expect(icon.attributes().title).toBe(
+ 'Re-import creates a new group. It does not sync with the existing group.',
+ );
+ });
+ });
+
+ it('does not render import button when group import is in progress', () => {
+ const group = generateFakeEntry({ id: 1, status: STATUSES.STARTED });
+ createComponent({ group });
+
+ const button = wrapper.findComponent(GlButton);
+ expect(button.exists()).toBe(false);
+ });
+
+ it('renders import button as disabled when there are validation errors', () => {
+ const group = generateFakeEntry({
+ id: 1,
+ status: STATUSES.NONE,
+ validation_errors: [{ field: 'new_name', message: 'something ' }],
+ });
+ createComponent({ group });
+
+ const button = wrapper.findComponent(GlButton);
+ expect(button.props().disabled).toBe(true);
+ });
+
+ it('emits import-group event when import button is clicked', () => {
+ const group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ createComponent({ group });
+
+ const button = wrapper.findComponent(GlButton);
+ button.vm.$emit('click');
+
+ expect(wrapper.emitted('import-group')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
new file mode 100644
index 00000000000..2a56efd1cbb
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
@@ -0,0 +1,59 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { STATUSES } from '~/import_entities/constants';
+import ImportSourceCell from '~/import_entities/import_groups/components/import_source_cell.vue';
+import { generateFakeEntry } from '../graphql/fixtures';
+
+describe('import source cell', () => {
+ let wrapper;
+ let group;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(ImportSourceCell, {
+ propsData: {
+ ...props,
+ },
+ stubs: { GlSprintf },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when group status is NONE', () => {
+ beforeEach(() => {
+ group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ createComponent({ group });
+ });
+
+ it('renders link to a group', () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes().href).toBe(group.web_url);
+ expect(link.text()).toContain(group.full_path);
+ });
+
+ it('does not render last imported line', () => {
+ expect(wrapper.text()).not.toContain('Last imported to');
+ });
+ });
+
+ describe('when group status is FINISHED', () => {
+ beforeEach(() => {
+ group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED });
+ createComponent({ group });
+ });
+
+ it('renders link to a group', () => {
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes().href).toBe(group.web_url);
+ expect(link.text()).toContain(group.full_path);
+ });
+
+ it('renders last imported line', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(
+ 'fake_group_1 Last imported to root/last-group1',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index bbd8463e685..f43e545e049 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -15,6 +15,7 @@ import stubChildren from 'helpers/stub_children';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { STATUSES } from '~/import_entities/constants';
+import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
@@ -163,11 +164,8 @@ describe('import table', () => {
it('invokes importGroups mutation when row button is clicked', async () => {
jest.spyOn(apolloProvider.defaultClient, 'mutate');
- const triggerImportButton = wrapper
- .findAllComponents(GlButton)
- .wrappers.find((w) => w.text() === 'Import');
- triggerImportButton.vm.$emit('click');
+ wrapper.findComponent(ImportActionsCell).vm.$emit('import-group');
await waitForPromises();
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
@@ -329,7 +327,7 @@ describe('import table', () => {
});
it('does not allow selecting already started groups', async () => {
- const NEW_GROUPS = [generateFakeEntry({ id: 1, status: STATUSES.FINISHED })];
+ const NEW_GROUPS = [generateFakeEntry({ id: 1, status: STATUSES.STARTED })];
createComponent({
bulkImportSourceGroups: () => ({
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index 8231297e594..be83a61841f 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -1,14 +1,10 @@
-import { GlButton, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui';
+import { GlDropdownItem, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import { availableNamespacesFixture } from '../graphql/fixtures';
-Vue.use(VueApollo);
-
const getFakeGroup = (status) => ({
web_url: 'https://fake.host/',
full_path: 'fake_group_1',
@@ -26,9 +22,6 @@ describe('import target cell', () => {
let wrapper;
let group;
- const findByText = (cmp, text) => {
- return wrapper.findAll(cmp).wrappers.find((node) => node.text().indexOf(text) === 0);
- };
const findNameInput = () => wrapper.find(GlFormInput);
const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
@@ -117,10 +110,6 @@ describe('import target cell', () => {
createComponent({ group });
});
- it('does not render Import button', () => {
- expect(findByText(GlButton, 'Import')).toBe(undefined);
- });
-
it('renders namespace dropdown as disabled', () => {
expect(findNamespaceDropdown().attributes('disabled')).toBe('true');
});
@@ -132,17 +121,8 @@ describe('import target cell', () => {
createComponent({ group });
});
- it('does not render Import button', () => {
- expect(findByText(GlButton, 'Import')).toBe(undefined);
- });
-
- it('does not render namespace dropdown', () => {
- expect(findNamespaceDropdown().exists()).toBe(false);
- });
-
- it('renders target as link', () => {
- const TARGET_LINK = `${group.import_target.target_namespace}/${group.import_target.new_name}`;
- expect(findByText(GlLink, TARGET_LINK).exists()).toBe(true);
+ it('renders namespace dropdown as enabled', () => {
+ expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined);
});
});
@@ -179,9 +159,6 @@ describe('import target cell', () => {
},
});
- jest.runOnlyPendingTimers();
- await nextTick();
-
expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE);
});
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index ec50dfd037f..e1d65095888 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -259,6 +259,10 @@ describe('Bulk import resolvers', () => {
target_namespace: 'root',
new_name: 'group1',
},
+ last_import_target: {
+ target_namespace: 'root',
+ new_name: 'group1',
+ },
validation_errors: [],
},
],
@@ -414,19 +418,32 @@ describe('Bulk import resolvers', () => {
});
});
- it('setImportProgress updates group progress', async () => {
+ it('setImportProgress updates group progress and sets import target', async () => {
const NEW_STATUS = 'dummy';
const FAKE_JOB_ID = 5;
+ const IMPORT_TARGET = {
+ __typename: 'ClientBulkImportTarget',
+ new_name: 'fake_name',
+ target_namespace: 'fake_target',
+ };
const {
data: {
- setImportProgress: { progress },
+ setImportProgress: { progress, last_import_target: lastImportTarget },
},
} = await client.mutate({
mutation: setImportProgressMutation,
- variables: { sourceGroupId: GROUP_ID, status: NEW_STATUS, jobId: FAKE_JOB_ID },
+ variables: {
+ sourceGroupId: GROUP_ID,
+ status: NEW_STATUS,
+ jobId: FAKE_JOB_ID,
+ importTarget: IMPORT_TARGET,
+ },
});
- expect(progress).toMatchObject({
+ expect(lastImportTarget).toStrictEqual(IMPORT_TARGET);
+
+ expect(progress).toStrictEqual({
+ __typename: clientTypenames.BulkImportProgress,
id: FAKE_JOB_ID,
status: NEW_STATUS,
});
@@ -442,7 +459,8 @@ describe('Bulk import resolvers', () => {
variables: { id: FAKE_JOB_ID, status: NEW_STATUS },
});
- expect(statusInResponse).toMatchObject({
+ expect(statusInResponse).toStrictEqual({
+ __typename: clientTypenames.BulkImportProgress,
id: FAKE_JOB_ID,
status: NEW_STATUS,
});
@@ -460,7 +478,13 @@ describe('Bulk import resolvers', () => {
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
});
- expect(validationErrors).toMatchObject([{ field: FAKE_FIELD, message: FAKE_MESSAGE }]);
+ expect(validationErrors).toStrictEqual([
+ {
+ __typename: clientTypenames.BulkImportValidationError,
+ field: FAKE_FIELD,
+ message: FAKE_MESSAGE,
+ },
+ ]);
});
it('removeValidationError removes error from group', async () => {
@@ -481,7 +505,7 @@ describe('Bulk import resolvers', () => {
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD },
});
- expect(validationErrors).toMatchObject([]);
+ expect(validationErrors).toStrictEqual([]);
});
});
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
index 6f66066b312..d1bd52693b6 100644
--- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -9,6 +9,10 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({
target_namespace: 'root',
new_name: `group${id}`,
},
+ last_import_target: {
+ target_namespace: 'root',
+ new_name: `last-group${id}`,
+ },
id,
progress: {
id: `test-${id}`,
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
index bae715edac0..f06babcb149 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
@@ -20,7 +20,7 @@ describe('SourceGroupsManager', () => {
describe('storage management', () => {
const IMPORT_ID = 1;
- const IMPORT_TARGET = { destination_name: 'demo', destination_namespace: 'foo' };
+ const IMPORT_TARGET = { new_name: 'demo', target_namespace: 'foo' };
const STATUS = 'FAKE_STATUS';
const FAKE_GROUP = { id: 1, import_target: IMPORT_TARGET, status: STATUS };
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index f2bfc61381c..0ebe8525b5a 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -85,7 +85,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
- it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
+ it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(200, payload);
return testAction(
@@ -93,8 +93,8 @@ describe('import_projects store actions', () => {
null,
localState,
[
- { type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 1 },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
@@ -104,19 +104,14 @@ describe('import_projects store actions', () => {
);
});
- it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_ERROR and SET_PAGE again mutations on an unsuccessful request', () => {
+ it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
return testAction(
fetchRepos,
null,
localState,
- [
- { type: SET_PAGE, payload: 1 },
- { type: REQUEST_REPOS },
- { type: SET_PAGE, payload: 0 },
- { type: RECEIVE_REPOS_ERROR },
- ],
+ [{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
});
@@ -135,7 +130,7 @@ describe('import_projects store actions', () => {
expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
});
- it('correctly updates current page on an unsuccessful request', () => {
+ it('correctly keeps current page on an unsuccessful request', () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
const CURRENT_PAGE = 5;
@@ -143,10 +138,7 @@ describe('import_projects store actions', () => {
fetchRepos,
null,
{ ...localState, pageInfo: { page: CURRENT_PAGE } },
- expect.arrayContaining([
- { type: SET_PAGE, payload: CURRENT_PAGE + 1 },
- { type: SET_PAGE, payload: CURRENT_PAGE },
- ]),
+ expect.arrayContaining([]),
[],
);
});
@@ -159,12 +151,7 @@ describe('import_projects store actions', () => {
fetchRepos,
null,
{ ...localState, filter: 'filter' },
- [
- { type: SET_PAGE, payload: 1 },
- { type: REQUEST_REPOS },
- { type: SET_PAGE, payload: 0 },
- { type: RECEIVE_REPOS_ERROR },
- ],
+ [{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
@@ -183,8 +170,8 @@ describe('import_projects store actions', () => {
null,
{ ...localState, filter: 'filter' },
[
- { type: SET_PAGE, payload: 1 },
{ type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 1 },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
diff --git a/spec/frontend/invite_members/components/import_a_project_modal_spec.js b/spec/frontend/invite_members/components/import_a_project_modal_spec.js
new file mode 100644
index 00000000000..fecbf84fb57
--- /dev/null
+++ b/spec/frontend/invite_members/components/import_a_project_modal_spec.js
@@ -0,0 +1,167 @@
+import { GlFormGroup, GlSprintf, GlModal } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as ProjectsApi from '~/api/projects_api';
+import ImportAProjectModal from '~/invite_members/components/import_a_project_modal.vue';
+import ProjectSelect from '~/invite_members/components/project_select.vue';
+import axios from '~/lib/utils/axios_utils';
+
+let wrapper;
+let mock;
+
+const projectId = '1';
+const projectName = 'test name';
+const projectToBeImported = { id: '2' };
+const $toast = {
+ show: jest.fn(),
+};
+
+const createComponent = () => {
+ wrapper = shallowMountExtended(ImportAProjectModal, {
+ propsData: {
+ projectId,
+ projectName,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlSprintf,
+ GlFormGroup: stubComponent(GlFormGroup, {
+ props: ['state', 'invalidFeedback'],
+ }),
+ },
+ mocks: {
+ $toast,
+ },
+ });
+};
+
+beforeEach(() => {
+ gon.api_version = 'v4';
+ mock = new MockAdapter(axios);
+});
+
+afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+});
+
+describe('ImportAProjectModal', () => {
+ const findIntroText = () => wrapper.find({ ref: 'modalIntro' }).text();
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findImportButton = () => wrapper.findByTestId('import-button');
+ const clickImportButton = () => findImportButton().vm.$emit('click');
+ const clickCancelButton = () => findCancelButton().vm.$emit('click');
+ const findFormGroup = () => wrapper.findByTestId('form-group');
+ const formGroupInvalidFeedback = () => findFormGroup().props('invalidFeedback');
+ const formGroupErrorState = () => findFormGroup().props('state');
+ const findProjectSelect = () => wrapper.findComponent(ProjectSelect);
+
+ describe('rendering the modal', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the modal with the correct title', () => {
+ expect(wrapper.findComponent(GlModal).props('title')).toBe(
+ 'Import members from another project',
+ );
+ });
+
+ it('renders the Cancel button text correctly', () => {
+ expect(findCancelButton().text()).toBe('Cancel');
+ });
+
+ it('renders the Import button text correctly', () => {
+ expect(findImportButton().text()).toBe('Import project members');
+ });
+
+ it('renders the modal intro text correctly', () => {
+ expect(findIntroText()).toBe("You're importing members to the test name project.");
+ });
+
+ it('renders the Import button modal without isLoading', () => {
+ expect(findImportButton().props('loading')).toBe(false);
+ });
+
+ it('sets isLoading to true when the Invite button is clicked', async () => {
+ clickImportButton();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findImportButton().props('loading')).toBe(true);
+ });
+ });
+
+ describe('submitting the import form', () => {
+ describe('when the import is successful', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findProjectSelect().vm.$emit('input', projectToBeImported);
+
+ jest.spyOn(ProjectsApi, 'importProjectMembers').mockResolvedValue();
+
+ clickImportButton();
+ });
+
+ it('calls Api importProjectMembers', () => {
+ expect(ProjectsApi.importProjectMembers).toHaveBeenCalledWith(
+ projectId,
+ projectToBeImported.id,
+ );
+ });
+
+ it('displays the successful toastMessage', () => {
+ expect($toast.show).toHaveBeenCalledWith(
+ 'Successfully imported',
+ wrapper.vm.$options.toastOptions,
+ );
+ });
+
+ it('sets isLoading to false after success', () => {
+ expect(findImportButton().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when the import fails', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findProjectSelect().vm.$emit('input', projectToBeImported);
+
+ jest
+ .spyOn(ProjectsApi, 'importProjectMembers')
+ .mockRejectedValue({ response: { data: { success: false } } });
+
+ clickImportButton();
+ await waitForPromises();
+ });
+
+ it('displays the generic error message', () => {
+ expect(formGroupInvalidFeedback()).toBe('Unable to import project members');
+ expect(formGroupErrorState()).toBe(false);
+ });
+
+ it('sets isLoading to false after error', () => {
+ expect(findImportButton().props('loading')).toBe(false);
+ });
+
+ it('clears the error when the modal is closed with an error', async () => {
+ expect(formGroupInvalidFeedback()).toBe('Unable to import project members');
+ expect(formGroupErrorState()).toBe(false);
+
+ clickCancelButton();
+
+ await wrapper.vm.$nextTick();
+
+ expect(formGroupInvalidFeedback()).toBe('');
+ expect(formGroupErrorState()).not.toBe(false);
+ });
+ });
+ });
+});
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 f57af61ad5b..b2ebb9e4a47 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -79,14 +79,14 @@ describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement
it('does not add tracking attributes', () => {
createComponent();
- expect(findButton().attributes('data-track-event')).toBeUndefined();
+ expect(findButton().attributes('data-track-action')).toBeUndefined();
expect(findButton().attributes('data-track-label')).toBeUndefined();
});
it('adds tracking attributes', () => {
createComponent({ label: '_label_', event: '_event_' });
- expect(findButton().attributes('data-track-event')).toBe('_event_');
+ expect(findButton().attributes('data-track-action')).toBe('_event_');
expect(findButton().attributes('data-track-label')).toBe('_label_');
});
});
diff --git a/spec/frontend/invite_members/components/project_select_spec.js b/spec/frontend/invite_members/components/project_select_spec.js
new file mode 100644
index 00000000000..acc062b5fff
--- /dev/null
+++ b/spec/frontend/invite_members/components/project_select_spec.js
@@ -0,0 +1,105 @@
+import { GlSearchBoxByType, GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as projectsApi from '~/api/projects_api';
+import ProjectSelect from '~/invite_members/components/project_select.vue';
+import { allProjects, project1 } from '../mock_data/api_response_data';
+
+describe('ProjectSelect', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProjectSelect, {});
+ };
+
+ beforeEach(() => {
+ jest.spyOn(projectsApi, 'getProjects').mockResolvedValue(allProjects);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdownItem = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
+ const findAvatarLabeled = (index) => findDropdownItem(index).findComponent(GlAvatarLabeled);
+ const findEmptyResultMessage = () => wrapper.findByTestId('empty-result-message');
+ const findErrorMessage = () => wrapper.findByTestId('error-message');
+
+ it('renders GlSearchBoxByType with default attributes', () => {
+ expect(findSearchBoxByType().exists()).toBe(true);
+ expect(findSearchBoxByType().vm.$attrs).toMatchObject({
+ placeholder: 'Search projects',
+ });
+ });
+
+ describe('when user types in the search input', () => {
+ let resolveApiRequest;
+ let rejectApiRequest;
+
+ beforeEach(() => {
+ jest.spyOn(projectsApi, 'getProjects').mockImplementation(
+ () =>
+ new Promise((resolve, reject) => {
+ resolveApiRequest = resolve;
+ rejectApiRequest = reject;
+ }),
+ );
+
+ findSearchBoxByType().vm.$emit('input', project1.name);
+ });
+
+ it('calls the API', () => {
+ resolveApiRequest({ data: allProjects });
+
+ expect(projectsApi.getProjects).toHaveBeenCalledWith(project1.name, {
+ active: true,
+ exclude_internal: true,
+ });
+ });
+
+ it('displays loading icon while waiting for API call to resolve and then sets loading false', async () => {
+ expect(findSearchBoxByType().props('isLoading')).toBe(true);
+
+ resolveApiRequest({ data: allProjects });
+ await waitForPromises();
+
+ expect(findSearchBoxByType().props('isLoading')).toBe(false);
+ expect(findEmptyResultMessage().exists()).toBe(false);
+ expect(findErrorMessage().exists()).toBe(false);
+ });
+
+ it('displays a dropdown item and avatar for each project fetched', async () => {
+ resolveApiRequest({ data: allProjects });
+ await waitForPromises();
+
+ allProjects.forEach((project, index) => {
+ expect(findDropdownItem(index).attributes('name')).toBe(project.name_with_namespace);
+ expect(findAvatarLabeled(index).attributes()).toMatchObject({
+ src: project.avatar_url,
+ 'entity-id': String(project.id),
+ 'entity-name': project.name_with_namespace,
+ });
+ expect(findAvatarLabeled(index).props('label')).toBe(project.name_with_namespace);
+ });
+ });
+
+ it('displays the empty message when the API results are empty', async () => {
+ resolveApiRequest({ data: [] });
+ await waitForPromises();
+
+ expect(findEmptyResultMessage().text()).toBe('No matching results');
+ });
+
+ it('displays the error message when the fetch fails', async () => {
+ rejectApiRequest();
+ await waitForPromises();
+
+ expect(findErrorMessage().text()).toBe(
+ 'There was an error fetching the projects. Please try again.',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/invite_members/mock_data/api_response_data.js b/spec/frontend/invite_members/mock_data/api_response_data.js
new file mode 100644
index 00000000000..9509422b603
--- /dev/null
+++ b/spec/frontend/invite_members/mock_data/api_response_data.js
@@ -0,0 +1,13 @@
+export const project1 = {
+ id: 1,
+ name: 'Project One',
+ name_with_namespace: 'Project One',
+ avatar_url: 'test1',
+};
+export const project2 = {
+ id: 2,
+ name: 'Project One',
+ name_with_namespace: 'Project Two',
+ avatar_url: 'test2',
+};
+export const allProjects = [project1, project2];
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index babe3a66578..bd05cb1ac5a 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -1,7 +1,8 @@
import { GlIntersectionObserver } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import '~/behaviors/markdown/render_gfm';
import IssuableApp from '~/issue_show/components/app.vue';
import DescriptionComponent from '~/issue_show/components/description.vue';
@@ -33,13 +34,17 @@ describe('Issuable output', () => {
let realtimeRequestCount = 0;
let wrapper;
- const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]');
- const findLockedBadge = () => wrapper.find('[data-testid="locked"]');
- const findConfidentialBadge = () => wrapper.find('[data-testid="confidential"]');
+ const findStickyHeader = () => wrapper.findByTestId('issue-sticky-header');
+ const findLockedBadge = () => wrapper.findByTestId('locked');
+ const findConfidentialBadge = () => wrapper.findByTestId('confidential');
+ const findHiddenBadge = () => wrapper.findByTestId('hidden');
const findAlert = () => wrapper.find('.alert');
const mountComponent = (props = {}, options = {}, data = {}) => {
- wrapper = mount(IssuableApp, {
+ wrapper = mountExtended(IssuableApp, {
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
propsData: { ...appProps, ...props },
provide: {
fullPath: 'gitlab-org/incidents',
@@ -539,8 +544,8 @@ describe('Issuable output', () => {
it.each`
title | isConfidential
- ${'does not show confidential badge when issue is not confidential'} | ${true}
- ${'shows confidential badge when issue is confidential'} | ${false}
+ ${'does not show confidential badge when issue is not confidential'} | ${false}
+ ${'shows confidential badge when issue is confidential'} | ${true}
`('$title', async ({ isConfidential }) => {
wrapper.setProps({ isConfidential });
@@ -551,8 +556,8 @@ describe('Issuable output', () => {
it.each`
title | isLocked
- ${'does not show locked badge when issue is not locked'} | ${true}
- ${'shows locked badge when issue is locked'} | ${false}
+ ${'does not show locked badge when issue is not locked'} | ${false}
+ ${'shows locked badge when issue is locked'} | ${true}
`('$title', async ({ isLocked }) => {
wrapper.setProps({ isLocked });
@@ -560,6 +565,27 @@ describe('Issuable output', () => {
expect(findLockedBadge().exists()).toBe(isLocked);
});
+
+ it.each`
+ title | isHidden
+ ${'does not show hidden badge when issue is not hidden'} | ${false}
+ ${'shows hidden badge when issue is hidden'} | ${true}
+ `('$title', async ({ isHidden }) => {
+ wrapper.setProps({ isHidden });
+
+ await nextTick();
+
+ const hiddenBadge = findHiddenBadge();
+
+ expect(hiddenBadge.exists()).toBe(isHidden);
+
+ if (isHidden) {
+ expect(hiddenBadge.attributes('title')).toBe(
+ 'This issue is hidden because its author has been banned',
+ );
+ expect(getBinding(hiddenBadge.element, 'gl-tooltip')).not.toBeUndefined();
+ }
+ });
});
});
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 0cb1092135f..8d79a5eed35 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -5,17 +5,17 @@ import { cloneDeep } from 'lodash';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
-import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import {
+ getIssuesCountsQueryResponse,
getIssuesQueryResponse,
filteredTokens,
locationSearch,
urlParams,
- getIssuesCountQueryResponse,
} from 'jest/issues_list/mock_data';
import createFlash from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -63,15 +63,15 @@ describe('IssuesListApp component', () => {
canBulkUpdate: false,
emptyStateSvgPath: 'empty-state.svg',
exportCsvPath: 'export/csv/path',
+ fullPath: 'path/to/project',
+ hasAnyIssues: true,
hasBlockedIssuesFeature: true,
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
- hasProjectIssues: true,
+ isProject: true,
isSignedIn: true,
- issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
- projectPath: 'path/to/project',
rssPath: 'rss/path',
showNewIssueLink: true,
signInPath: 'sign/in/path',
@@ -97,12 +97,12 @@ describe('IssuesListApp component', () => {
const mountComponent = ({
provide = {},
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
- issuesQueryCountResponse = jest.fn().mockResolvedValue(getIssuesCountQueryResponse),
+ issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
- [getIssuesCountQuery, issuesQueryCountResponse],
+ [getIssuesCountsQuery, issuesCountsQueryResponse],
];
const apolloProvider = createMockApollo(requestHandlers);
@@ -134,7 +134,7 @@ describe('IssuesListApp component', () => {
it('renders', () => {
expect(findIssuableList().props()).toMatchObject({
- namespace: defaultProvide.projectPath,
+ namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
sortOptions: getSortOptions(true, true),
@@ -191,7 +191,7 @@ describe('IssuesListApp component', () => {
setWindowLocation(search);
wrapper = mountComponent({
- provide: { ...defaultProvide, isSignedIn: true },
+ provide: { isSignedIn: true },
mountFn: mount,
});
@@ -208,7 +208,15 @@ describe('IssuesListApp component', () => {
describe('when user is not signed in', () => {
it('does not render', () => {
- wrapper = mountComponent({ provide: { ...defaultProvide, isSignedIn: false } });
+ wrapper = mountComponent({ provide: { isSignedIn: false } });
+
+ expect(findCsvImportExportButtons().exists()).toBe(false);
+ });
+ });
+
+ describe('when in a group context', () => {
+ it('does not render', () => {
+ wrapper = mountComponent({ provide: { isProject: false } });
expect(findCsvImportExportButtons().exists()).toBe(false);
});
@@ -349,7 +357,7 @@ describe('IssuesListApp component', () => {
beforeEach(() => {
setWindowLocation(`?search=no+results`);
- wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
+ wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
});
it('shows empty state', () => {
@@ -363,7 +371,7 @@ describe('IssuesListApp component', () => {
describe('when "Open" tab has no issues', () => {
beforeEach(() => {
- wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
+ wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
});
it('shows empty state', () => {
@@ -379,7 +387,7 @@ describe('IssuesListApp component', () => {
beforeEach(() => {
setWindowLocation(`?state=${IssuableStates.Closed}`);
- wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
+ wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
});
it('shows empty state', () => {
@@ -395,7 +403,7 @@ describe('IssuesListApp component', () => {
describe('when user is logged in', () => {
beforeEach(() => {
wrapper = mountComponent({
- provide: { hasProjectIssues: false, isSignedIn: true },
+ provide: { hasAnyIssues: false, isSignedIn: true },
mountFn: mount,
});
});
@@ -434,7 +442,7 @@ describe('IssuesListApp component', () => {
describe('when user is logged out', () => {
beforeEach(() => {
wrapper = mountComponent({
- provide: { hasProjectIssues: false, isSignedIn: false },
+ provide: { hasAnyIssues: false, isSignedIn: false },
});
});
@@ -571,9 +579,9 @@ describe('IssuesListApp component', () => {
describe('errors', () => {
describe.each`
- error | mountOption | message
- ${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
- ${'fetching issue counts'} | ${'issuesQueryCountResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
+ error | mountOption | message
+ ${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
+ ${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
beforeEach(() => {
wrapper = mountComponent({
@@ -625,78 +633,99 @@ describe('IssuesListApp component', () => {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/1',
iid: '101',
- title: 'Issue one',
+ reference: 'group/project#1',
+ webPath: '/group/project/-/issues/1',
};
const issueTwo = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/2',
iid: '102',
- title: 'Issue two',
+ reference: 'group/project#2',
+ webPath: '/group/project/-/issues/2',
};
const issueThree = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/3',
iid: '103',
- title: 'Issue three',
+ reference: 'group/project#3',
+ webPath: '/group/project/-/issues/3',
};
const issueFour = {
...defaultQueryResponse.data.project.issues.nodes[0],
id: 'gid://gitlab/Issue/4',
iid: '104',
- title: 'Issue four',
+ reference: 'group/project#4',
+ webPath: '/group/project/-/issues/4',
};
- const response = {
+ const response = (isProject = true) => ({
data: {
- project: {
+ [isProject ? 'project' : 'group']: {
issues: {
...defaultQueryResponse.data.project.issues,
nodes: [issueOne, issueTwo, issueThree, issueFour],
},
},
},
- };
-
- beforeEach(() => {
- wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockResolvedValue(response) });
- jest.runOnlyPendingTimers();
});
describe('when successful', () => {
- describe.each`
- description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId
- ${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id}
- ${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id}
- ${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id}
- ${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null}
- `(
- 'when moving issue $description',
- ({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
- it('makes API call to reorder the issue', async () => {
- findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
-
- await waitForPromises();
-
- expect(axiosMock.history.put[0]).toMatchObject({
- url: joinPaths(defaultProvide.issuesPath, issueToMove.iid, 'reorder'),
- data: JSON.stringify({
- move_before_id: getIdFromGraphQLId(moveBeforeId),
- move_after_id: getIdFromGraphQLId(moveAfterId),
- }),
+ describe.each([true, false])('when isProject=%s', (isProject) => {
+ describe.each`
+ description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId
+ ${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id}
+ ${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id}
+ ${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id}
+ ${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null}
+ `(
+ 'when moving issue $description',
+ ({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: { isProject },
+ issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)),
+ });
+ jest.runOnlyPendingTimers();
});
- });
- },
- );
+
+ it('makes API call to reorder the issue', async () => {
+ findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
+
+ await waitForPromises();
+
+ expect(axiosMock.history.put[0]).toMatchObject({
+ url: joinPaths(issueToMove.webPath, 'reorder'),
+ data: JSON.stringify({
+ move_before_id: getIdFromGraphQLId(moveBeforeId),
+ move_after_id: getIdFromGraphQLId(moveAfterId),
+ group_full_path: isProject ? undefined : defaultProvide.fullPath,
+ }),
+ });
+ });
+ },
+ );
+ });
});
describe('when unsuccessful', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ issuesQueryResponse: jest.fn().mockResolvedValue(response()),
+ });
+ jest.runOnlyPendingTimers();
+ });
+
it('displays an error message', async () => {
- axiosMock.onPut(joinPaths(defaultProvide.issuesPath, issueOne.iid, 'reorder')).reply(500);
+ axiosMock.onPut(joinPaths(issueOne.webPath, 'reorder')).reply(500);
findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({ message: IssuesListApp.i18n.reorderError });
+ expect(createFlash).toHaveBeenCalledWith({
+ message: IssuesListApp.i18n.reorderError,
+ captureError: true,
+ error: new Error('Request failed with status code 500'),
+ });
});
});
});
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index d3f3f2f9f23..720f9cac986 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -29,6 +29,7 @@ export const getIssuesQueryResponse = {
updatedAt: '2021-05-22T04:08:01Z',
upvotes: 3,
userDiscussionsCount: 4,
+ webPath: 'project/-/issues/789',
webUrl: 'project/-/issues/789',
assignees: {
nodes: [
@@ -70,10 +71,16 @@ export const getIssuesQueryResponse = {
},
};
-export const getIssuesCountQueryResponse = {
+export const getIssuesCountsQueryResponse = {
data: {
project: {
- issues: {
+ openedIssues: {
+ count: 1,
+ },
+ closedIssues: {
+ count: 1,
+ },
+ allIssues: {
count: 1,
},
},
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index f2142ce1fcf..891ba9c223c 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -128,8 +128,26 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5"
+ >
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+ </div>
+
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+ </div>
+ </div>
+
+ <div
class="gl-new-dropdown-contents"
>
+ <!---->
+
<div
class="gl-search-box-by-type"
>
@@ -255,8 +273,26 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-px-5"
+ >
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+ </div>
+
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+ </div>
+ </div>
+
+ <div
class="gl-new-dropdown-contents"
>
+ <!---->
+
<div
class="gl-search-box-by-type"
>
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 1f4dd7d6216..f8a0059bf21 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -140,7 +140,7 @@ describe('Job App', () => {
it('should render provided job information', () => {
expect(wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim()).toContain(
- 'passed Job #4757 triggered 1 year ago by Root',
+ 'passed Job test triggered 1 year ago by Root',
);
});
@@ -154,7 +154,7 @@ describe('Job App', () => {
setupAndMount().then(() => {
expect(
wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim(),
- ).toContain('passed Job #4757 created 3 weeks ago by Root');
+ ).toContain('passed Job test created 3 weeks ago by Root');
}));
});
});
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
new file mode 100644
index 00000000000..1b1e2d4df8f
--- /dev/null
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -0,0 +1,126 @@
+import { GlModal } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ActionsCell from '~/jobs/components/table/cells/actions_cell.vue';
+import JobPlayMutation from '~/jobs/components/table/graphql/mutations/job_play.mutation.graphql';
+import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retry.mutation.graphql';
+import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql';
+import { playableJob, retryableJob, scheduledJob } from '../../../mock_data';
+
+describe('Job actions cell', () => {
+ let wrapper;
+ let mutate;
+
+ const findRetryButton = () => wrapper.findByTestId('retry');
+ const findPlayButton = () => wrapper.findByTestId('play');
+ const findDownloadArtifactsButton = () => wrapper.findByTestId('download-artifacts');
+ const findCountdownButton = () => wrapper.findByTestId('countdown');
+ const findPlayScheduledJobButton = () => wrapper.findByTestId('play-scheduled');
+ const findUnscheduleButton = () => wrapper.findByTestId('unschedule');
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const MUTATION_SUCCESS = { data: { JobRetryMutation: { jobId: retryableJob.id } } };
+ const MUTATION_SUCCESS_UNSCHEDULE = {
+ data: { JobUnscheduleMutation: { jobId: scheduledJob.id } },
+ };
+ const MUTATION_SUCCESS_PLAY = { data: { JobPlayMutation: { jobId: playableJob.id } } };
+
+ const $toast = {
+ show: jest.fn(),
+ };
+
+ const createComponent = (jobType, mutationType = MUTATION_SUCCESS, props = {}) => {
+ mutate = jest.fn().mockResolvedValue(mutationType);
+
+ wrapper = shallowMountExtended(ActionsCell, {
+ propsData: {
+ job: jobType,
+ ...props,
+ },
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ $toast,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not display an artifacts download button', () => {
+ createComponent(retryableJob);
+
+ expect(findDownloadArtifactsButton().exists()).toBe(false);
+ });
+
+ it.each`
+ button | action | jobType
+ ${findPlayButton} | ${'play'} | ${playableJob}
+ ${findRetryButton} | ${'retry'} | ${retryableJob}
+ ${findDownloadArtifactsButton} | ${'download artifacts'} | ${playableJob}
+ `('displays the $action button', ({ button, jobType }) => {
+ createComponent(jobType);
+
+ expect(button().exists()).toBe(true);
+ });
+
+ it.each`
+ button | mutationResult | action | jobType | mutationFile
+ ${findPlayButton} | ${MUTATION_SUCCESS_PLAY} | ${'play'} | ${playableJob} | ${JobPlayMutation}
+ ${findRetryButton} | ${MUTATION_SUCCESS} | ${'retry'} | ${retryableJob} | ${JobRetryMutation}
+ `('performs the $action mutation', ({ button, mutationResult, jobType, mutationFile }) => {
+ createComponent(jobType, mutationResult);
+
+ button().vm.$emit('click');
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: mutationFile,
+ variables: {
+ id: jobType.id,
+ },
+ });
+ });
+
+ describe('Scheduled Jobs', () => {
+ const today = () => new Date('2021-08-31');
+
+ beforeEach(() => {
+ jest.spyOn(Date, 'now').mockImplementation(today);
+ });
+
+ it('displays the countdown, play and unschedule buttons', () => {
+ createComponent(scheduledJob);
+
+ expect(findCountdownButton().exists()).toBe(true);
+ expect(findPlayScheduledJobButton().exists()).toBe(true);
+ expect(findUnscheduleButton().exists()).toBe(true);
+ });
+
+ it('unschedules a job', () => {
+ createComponent(scheduledJob, MUTATION_SUCCESS_UNSCHEDULE);
+
+ findUnscheduleButton().vm.$emit('click');
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: JobUnscheduleMutation,
+ variables: {
+ id: scheduledJob.id,
+ },
+ });
+ });
+
+ it('shows the play job confirmation modal', async () => {
+ createComponent(scheduledJob, MUTATION_SUCCESS);
+
+ findPlayScheduledJobButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findModal().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
index 763a4b0eaa2..763a4b0eaa2 100644
--- a/spec/frontend/jobs/components/table/cells.vue/duration_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
diff --git a/spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
index fc4e5586349..fc4e5586349 100644
--- a/spec/frontend/jobs/components/table/cells.vue/job_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
diff --git a/spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
index 1f5e0a7aa21..1f5e0a7aa21 100644
--- a/spec/frontend/jobs/components/table/cells.vue/pipeline_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 57f0b852ff8..43755b46bc9 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1555,7 +1555,11 @@ export const mockJobsQueryResponse = {
cancelable: false,
active: false,
stuck: false,
- userPermissions: { readBuild: true, __typename: 'JobPermissions' },
+ userPermissions: {
+ readBuild: true,
+ readJobArtifacts: true,
+ __typename: 'JobPermissions',
+ },
__typename: 'CiJob',
},
],
@@ -1573,3 +1577,179 @@ export const mockJobsQueryEmptyResponse = {
},
},
};
+
+export const retryableJob = {
+ artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' },
+ allowFailure: false,
+ status: 'SUCCESS',
+ scheduledAt: null,
+ manualJob: false,
+ triggered: null,
+ createdByTag: false,
+ detailedStatus: {
+ detailsPath: '/root/test-job-artifacts/-/jobs/1981',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ action: {
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ method: 'post',
+ path: '/root/test-job-artifacts/-/jobs/1981/retry',
+ title: 'Retry',
+ __typename: 'StatusAction',
+ },
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/1981',
+ refName: 'main',
+ refPath: '/root/test-job-artifacts/-/commits/main',
+ tags: [],
+ shortSha: '75daf01b',
+ commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/288',
+ path: '/root/test-job-artifacts/-/pipelines/288',
+ user: {
+ webPath: '/root',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ __typename: 'UserCore',
+ },
+ __typename: 'Pipeline',
+ },
+ stage: { name: 'test', __typename: 'CiStage' },
+ name: 'hello_world',
+ duration: 7,
+ finishedAt: '2021-08-30T20:33:56Z',
+ coverage: null,
+ retryable: true,
+ playable: false,
+ cancelable: false,
+ active: false,
+ stuck: false,
+ userPermissions: { readBuild: true, __typename: 'JobPermissions' },
+ __typename: 'CiJob',
+};
+
+export const playableJob = {
+ artifacts: {
+ nodes: [
+ {
+ downloadPath: '/root/test-job-artifacts/-/jobs/1982/artifacts/download?file_type=trace',
+ __typename: 'CiJobArtifact',
+ },
+ ],
+ __typename: 'CiJobArtifactConnection',
+ },
+ allowFailure: false,
+ status: 'SUCCESS',
+ scheduledAt: null,
+ manualJob: true,
+ triggered: null,
+ createdByTag: false,
+ detailedStatus: {
+ detailsPath: '/root/test-job-artifacts/-/jobs/1982',
+ group: 'success',
+ icon: 'status_success',
+ label: 'manual play action',
+ text: 'passed',
+ tooltip: 'passed',
+ action: {
+ buttonTitle: 'Trigger this manual action',
+ icon: 'play',
+ method: 'post',
+ path: '/root/test-job-artifacts/-/jobs/1982/play',
+ title: 'Play',
+ __typename: 'StatusAction',
+ },
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/1982',
+ refName: 'main',
+ refPath: '/root/test-job-artifacts/-/commits/main',
+ tags: [],
+ shortSha: '75daf01b',
+ commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/288',
+ path: '/root/test-job-artifacts/-/pipelines/288',
+ user: {
+ webPath: '/root',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ __typename: 'UserCore',
+ },
+ __typename: 'Pipeline',
+ },
+ stage: { name: 'test', __typename: 'CiStage' },
+ name: 'hello_world_delayed',
+ duration: 6,
+ finishedAt: '2021-08-30T20:36:12Z',
+ coverage: null,
+ retryable: true,
+ playable: true,
+ cancelable: false,
+ active: false,
+ stuck: false,
+ userPermissions: { readBuild: true, readJobArtifacts: true, __typename: 'JobPermissions' },
+ __typename: 'CiJob',
+};
+
+export const scheduledJob = {
+ artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' },
+ allowFailure: false,
+ status: 'SCHEDULED',
+ scheduledAt: '2021-08-31T22:36:05Z',
+ manualJob: true,
+ triggered: null,
+ createdByTag: false,
+ detailedStatus: {
+ detailsPath: '/root/test-job-artifacts/-/jobs/1986',
+ group: 'scheduled',
+ icon: 'status_scheduled',
+ label: 'unschedule action',
+ text: 'delayed',
+ tooltip: 'delayed manual action (%{remainingTime})',
+ action: {
+ buttonTitle: 'Unschedule job',
+ icon: 'time-out',
+ method: 'post',
+ path: '/root/test-job-artifacts/-/jobs/1986/unschedule',
+ title: 'Unschedule',
+ __typename: 'StatusAction',
+ },
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/1986',
+ refName: 'main',
+ refPath: '/root/test-job-artifacts/-/commits/main',
+ tags: [],
+ shortSha: '75daf01b',
+ commitPath: '/root/test-job-artifacts/-/commit/75daf01b465e7eab5a04a315e44660c9a17c8055',
+ pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/290',
+ path: '/root/test-job-artifacts/-/pipelines/290',
+ user: {
+ webPath: '/root',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ __typename: 'UserCore',
+ },
+ __typename: 'Pipeline',
+ },
+ stage: { name: 'test', __typename: 'CiStage' },
+ name: 'hello_world_delayed',
+ duration: null,
+ finishedAt: null,
+ coverage: null,
+ retryable: false,
+ playable: true,
+ cancelable: false,
+ active: false,
+ stuck: false,
+ userPermissions: { readBuild: true, __typename: 'JobPermissions' },
+ __typename: 'CiJob',
+};
diff --git a/spec/frontend/learn_gitlab/track_learn_gitlab_spec.js b/spec/frontend/learn_gitlab/track_learn_gitlab_spec.js
deleted file mode 100644
index 3fb38a74c70..00000000000
--- a/spec/frontend/learn_gitlab/track_learn_gitlab_spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { mockTracking } from 'helpers/tracking_helper';
-import trackLearnGitlab from '~/learn_gitlab/track_learn_gitlab';
-
-describe('trackTrialUserErrors', () => {
- let spy;
-
- describe('when an error is present', () => {
- beforeEach(() => {
- spy = mockTracking('projects:learn_gitlab_index', document.body, jest.spyOn);
- });
-
- it('tracks the error message', () => {
- trackLearnGitlab();
-
- expect(spy).toHaveBeenCalledWith('projects:learn_gitlab:index', 'page_init', {
- label: 'learn_gitlab',
- property: 'Growth::Activation::Experiment::LearnGitLabB',
- });
- });
- });
-});
diff --git a/spec/frontend/lib/apollo/instrumentation_link_spec.js b/spec/frontend/lib/apollo/instrumentation_link_spec.js
new file mode 100644
index 00000000000..ef686129257
--- /dev/null
+++ b/spec/frontend/lib/apollo/instrumentation_link_spec.js
@@ -0,0 +1,54 @@
+import { testApolloLink } from 'helpers/test_apollo_link';
+import { getInstrumentationLink, FEATURE_CATEGORY_HEADER } from '~/lib/apollo/instrumentation_link';
+
+const TEST_FEATURE_CATEGORY = 'foo_feature';
+
+describe('~/lib/apollo/instrumentation_link', () => {
+ const setFeatureCategory = (val) => {
+ window.gon.feature_category = val;
+ };
+
+ afterEach(() => {
+ getInstrumentationLink.cache.clear();
+ });
+
+ describe('getInstrumentationLink', () => {
+ describe('with no gon.feature_category', () => {
+ beforeEach(() => {
+ setFeatureCategory(null);
+ });
+
+ it('returns null', () => {
+ expect(getInstrumentationLink()).toBe(null);
+ });
+ });
+
+ describe('with gon.feature_category', () => {
+ beforeEach(() => {
+ setFeatureCategory(TEST_FEATURE_CATEGORY);
+ });
+
+ it('returns memoized apollo link', () => {
+ const result = getInstrumentationLink();
+
+ // expect.any(ApolloLink) doesn't work for some reason...
+ expect(result).toHaveProp('request');
+ expect(result).toBe(getInstrumentationLink());
+ });
+
+ it('adds a feature category header from the returned apollo link', async () => {
+ const defaultHeaders = { Authorization: 'foo' };
+ const operation = await testApolloLink(getInstrumentationLink(), {
+ context: { headers: defaultHeaders },
+ });
+
+ const { headers } = operation.getContext();
+
+ expect(headers).toEqual({
+ ...defaultHeaders,
+ [FEATURE_CATEGORY_HEADER]: TEST_FEATURE_CATEGORY,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index fa8dbb12a08..324441fa2c9 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -44,6 +44,31 @@ describe('~/lib/dompurify', () => {
expect(sanitize('<strong></strong>', { ALLOWED_TAGS: [] })).toBe('');
});
+ describe('includes default configuration', () => {
+ it('with empty config', () => {
+ const svgIcon = '<svg width="100"><use></use></svg>';
+ expect(sanitize(svgIcon, {})).toBe(svgIcon);
+ });
+
+ it('with valid config', () => {
+ expect(sanitize('<a href="#" data-remote="true"></a>', { ALLOWED_TAGS: ['a'] })).toBe(
+ '<a href="#"></a>',
+ );
+ });
+ });
+
+ it("doesn't sanitize local references", () => {
+ const htmlHref = `<svg><use href="#some-element"></use></svg>`;
+ const htmlXlink = `<svg><use xlink:href="#some-element"></use></svg>`;
+
+ expect(sanitize(htmlHref)).toBe(htmlHref);
+ expect(sanitize(htmlXlink)).toBe(htmlXlink);
+ });
+
+ it("doesn't sanitize gl-emoji", () => {
+ expect(sanitize('<p><gl-emoji>💯</gl-emoji></p>')).toBe('<p><gl-emoji>💯</gl-emoji></p>');
+ });
+
describe.each`
type | gon
${'root'} | ${rootGon}
diff --git a/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap b/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap
new file mode 100644
index 00000000000..791ec05befd
--- /dev/null
+++ b/spec/frontend/lib/logger/__snapshots__/hello_spec.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`~/lib/logger/hello logHello console logs a friendly hello message 1`] = `
+Array [
+ Array [
+ "%cWelcome to GitLab!%c
+
+Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute!
+
+🤝 Contribute to GitLab: https://about.gitlab.com/community/contribute/
+🔎 Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new",
+ "padding-top: 0.5em; font-size: 2em;",
+ "padding-bottom: 0.5em;",
+ ],
+]
+`;
diff --git a/spec/frontend/lib/logger/hello_deferred_spec.js b/spec/frontend/lib/logger/hello_deferred_spec.js
new file mode 100644
index 00000000000..3233cbff0dc
--- /dev/null
+++ b/spec/frontend/lib/logger/hello_deferred_spec.js
@@ -0,0 +1,17 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { logHello } from '~/lib/logger/hello';
+import { logHelloDeferred } from '~/lib/logger/hello_deferred';
+
+jest.mock('~/lib/logger/hello');
+
+describe('~/lib/logger/hello_deferred', () => {
+ it('dynamically imports and calls logHello', async () => {
+ logHelloDeferred();
+
+ expect(logHello).not.toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(logHello).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/lib/logger/hello_spec.js b/spec/frontend/lib/logger/hello_spec.js
new file mode 100644
index 00000000000..39abe0e0dd0
--- /dev/null
+++ b/spec/frontend/lib/logger/hello_spec.js
@@ -0,0 +1,20 @@
+import { logHello } from '~/lib/logger/hello';
+
+describe('~/lib/logger/hello', () => {
+ let consoleLogSpy;
+
+ beforeEach(() => {
+ // We don't `mockImplementation` so we can validate there's no errors thrown
+ consoleLogSpy = jest.spyOn(console, 'log');
+ });
+
+ describe('logHello', () => {
+ it('console logs a friendly hello message', () => {
+ expect(consoleLogSpy).not.toHaveBeenCalled();
+
+ logHello();
+
+ expect(consoleLogSpy.mock.calls).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/lib/logger/index_spec.js b/spec/frontend/lib/logger/index_spec.js
new file mode 100644
index 00000000000..9382fafe4de
--- /dev/null
+++ b/spec/frontend/lib/logger/index_spec.js
@@ -0,0 +1,23 @@
+import { logError, LOG_PREFIX } from '~/lib/logger';
+
+describe('~/lib/logger', () => {
+ let consoleErrorSpy;
+
+ beforeEach(() => {
+ consoleErrorSpy = jest.spyOn(console, 'error');
+ consoleErrorSpy.mockImplementation();
+ });
+
+ describe('logError', () => {
+ it('sends given message to console.error', () => {
+ const message = 'Lorem ipsum dolar sit amit';
+ const error = new Error('lorem ipsum');
+
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
+
+ logError(message, error);
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith(LOG_PREFIX, `${message}\n`, error);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/accessor_spec.js b/spec/frontend/lib/utils/accessor_spec.js
index 752a88296e6..63497d795ce 100644
--- a/spec/frontend/lib/utils/accessor_spec.js
+++ b/spec/frontend/lib/utils/accessor_spec.js
@@ -6,60 +6,9 @@ describe('AccessorUtilities', () => {
const testError = new Error('test error');
- describe('isPropertyAccessSafe', () => {
- let base;
-
- it('should return `true` if access is safe', () => {
- base = {
- testProp: 'testProp',
- };
- expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(true);
- });
-
- it('should return `false` if access throws an error', () => {
- base = {
- get testProp() {
- throw testError;
- },
- };
-
- expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
- });
-
- it('should return `false` if property is undefined', () => {
- base = {};
-
- expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
- });
- });
-
- describe('isFunctionCallSafe', () => {
- const base = {};
-
- it('should return `true` if calling is safe', () => {
- base.func = () => {};
-
- expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(true);
- });
-
- it('should return `false` if calling throws an error', () => {
- base.func = () => {
- throw new Error('test error');
- };
-
- expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
- });
-
- it('should return `false` if function is undefined', () => {
- base.func = undefined;
-
- expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
- });
- });
-
- describe('isLocalStorageAccessSafe', () => {
+ describe('canUseLocalStorage', () => {
it('should return `true` if access is safe', () => {
- expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(true);
+ expect(AccessorUtilities.canUseLocalStorage()).toBe(true);
});
it('should return `false` if access to .setItem isnt safe', () => {
@@ -67,19 +16,19 @@ describe('AccessorUtilities', () => {
throw testError;
});
- expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(false);
+ expect(AccessorUtilities.canUseLocalStorage()).toBe(false);
});
it('should set a test item if access is safe', () => {
- AccessorUtilities.isLocalStorageAccessSafe();
+ AccessorUtilities.canUseLocalStorage();
- expect(window.localStorage.setItem).toHaveBeenCalledWith('isLocalStorageAccessSafe', 'true');
+ expect(window.localStorage.setItem).toHaveBeenCalledWith('canUseLocalStorage', 'true');
});
it('should remove the test item if access is safe', () => {
- AccessorUtilities.isLocalStorageAccessSafe();
+ AccessorUtilities.canUseLocalStorage();
- expect(window.localStorage.removeItem).toHaveBeenCalledWith('isLocalStorageAccessSafe');
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith('canUseLocalStorage');
});
});
});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
new file mode 100644
index 00000000000..942ba56196e
--- /dev/null
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -0,0 +1,120 @@
+import * as utils from '~/lib/utils/datetime/date_format_utility';
+
+describe('date_format_utility.js', () => {
+ describe('padWithZeros', () => {
+ it.each`
+ input | output
+ ${0} | ${'00'}
+ ${'1'} | ${'01'}
+ ${'10'} | ${'10'}
+ ${'100'} | ${'100'}
+ ${100} | ${'100'}
+ ${'a'} | ${'0a'}
+ ${'foo'} | ${'foo'}
+ `('properly pads $input to match $output', ({ input, output }) => {
+ expect(utils.padWithZeros(input)).toEqual([output]);
+ });
+
+ it('accepts multiple arguments', () => {
+ expect(utils.padWithZeros(1, '2', 3)).toEqual(['01', '02', '03']);
+ });
+
+ it('returns an empty array provided no argument', () => {
+ expect(utils.padWithZeros()).toEqual([]);
+ });
+ });
+
+ describe('stripTimezoneFromISODate', () => {
+ it.each`
+ input | expectedOutput
+ ${'2021-08-16T00:00:00Z'} | ${'2021-08-16T00:00:00'}
+ ${'2021-08-16T10:30:00+02:00'} | ${'2021-08-16T10:30:00'}
+ ${'2021-08-16T10:30:00-05:30'} | ${'2021-08-16T10:30:00'}
+ `('returns $expectedOutput when given $input', ({ input, expectedOutput }) => {
+ expect(utils.stripTimezoneFromISODate(input)).toBe(expectedOutput);
+ });
+
+ it('returns null if date is invalid', () => {
+ expect(utils.stripTimezoneFromISODate('Invalid date')).toBe(null);
+ });
+ });
+
+ describe('dateToYearMonthDate', () => {
+ it.each`
+ date | expectedOutput
+ ${new Date('2021-08-05')} | ${{ year: '2021', month: '08', day: '05' }}
+ ${new Date('2021-12-24')} | ${{ year: '2021', month: '12', day: '24' }}
+ `('returns $expectedOutput provided $date', ({ date, expectedOutput }) => {
+ expect(utils.dateToYearMonthDate(date)).toEqual(expectedOutput);
+ });
+
+ it('throws provided an invalid date', () => {
+ expect(() => utils.dateToYearMonthDate('Invalid date')).toThrow(
+ 'Argument should be a Date instance',
+ );
+ });
+ });
+
+ describe('timeToHoursMinutes', () => {
+ it.each`
+ time | expectedOutput
+ ${'23:12'} | ${{ hours: '23', minutes: '12' }}
+ ${'23:12'} | ${{ hours: '23', minutes: '12' }}
+ `('returns $expectedOutput provided $time', ({ time, expectedOutput }) => {
+ expect(utils.timeToHoursMinutes(time)).toEqual(expectedOutput);
+ });
+
+ it('throws provided an invalid time', () => {
+ expect(() => utils.timeToHoursMinutes('Invalid time')).toThrow('Invalid time provided');
+ });
+ });
+
+ describe('dateAndTimeToISOString', () => {
+ it('computes the date properly', () => {
+ expect(utils.dateAndTimeToISOString(new Date('2021-08-16'), '10:00')).toBe(
+ '2021-08-16T10:00:00.000Z',
+ );
+ });
+
+ it('computes the date properly with an offset', () => {
+ expect(utils.dateAndTimeToISOString(new Date('2021-08-16'), '10:00', '-04:00')).toBe(
+ '2021-08-16T10:00:00.000-04:00',
+ );
+ });
+
+ it('throws if date in invalid', () => {
+ expect(() => utils.dateAndTimeToISOString('Invalid date', '10:00')).toThrow(
+ 'Argument should be a Date instance',
+ );
+ });
+
+ it('throws if time in invalid', () => {
+ expect(() => utils.dateAndTimeToISOString(new Date('2021-08-16'), '')).toThrow(
+ 'Invalid time provided',
+ );
+ });
+
+ it('throws if offset is invalid', () => {
+ expect(() =>
+ utils.dateAndTimeToISOString(new Date('2021-08-16'), '10:00', 'not an offset'),
+ ).toThrow('Could not initialize date');
+ });
+ });
+
+ describe('dateToTimeInputValue', () => {
+ it.each`
+ input | expectedOutput
+ ${new Date('2021-08-16T10:00:00.000Z')} | ${'10:00'}
+ ${new Date('2021-08-16T22:30:00.000Z')} | ${'22:30'}
+ ${new Date('2021-08-16T22:30:00.000-03:00')} | ${'01:30'}
+ `('extracts $expectedOutput out of $input', ({ input, expectedOutput }) => {
+ expect(utils.dateToTimeInputValue(input)).toBe(expectedOutput);
+ });
+
+ it('throws if date is invalid', () => {
+ expect(() => utils.dateToTimeInputValue('Invalid date')).toThrow(
+ 'Argument should be a Date instance',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index 7c4c20e651f..cb8b1c7ca9a 100644
--- a/spec/frontend/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -5,6 +5,7 @@ import {
parseBooleanDataAttributes,
isElementVisible,
isElementHidden,
+ getParents,
} from '~/lib/utils/dom_utils';
const TEST_MARGIN = 5;
@@ -193,4 +194,18 @@ describe('DOM Utils', () => {
});
},
);
+
+ describe('getParents', () => {
+ it('gets all parents of an element', () => {
+ const el = document.createElement('div');
+ el.innerHTML = '<p><span><strong><mark>hello world';
+
+ expect(getParents(el.querySelector('mark'))).toEqual([
+ el.querySelector('strong'),
+ el.querySelector('span'),
+ el.querySelector('p'),
+ el,
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index beedb9b2eba..acbf1a975b8 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -88,6 +88,25 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(`${initialValue}\n- `);
});
+ it('unescapes new line characters', () => {
+ const initialValue = '';
+
+ textArea.value = initialValue;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = 0;
+
+ insertMarkdownText({
+ textArea,
+ text: textArea.value,
+ tag: '```suggestion:-0+0\n{text}\n```',
+ blockTag: true,
+ selected: '# Does not parse the %br currently.',
+ wrap: false,
+ });
+
+ expect(textArea.value).toContain('# Does not parse the \\n currently.');
+ });
+
it('inserts the tag on the same line if the current line only contains spaces', () => {
const initialValue = ' ';
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index c8ac7ffc9d9..6f186ba3227 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -645,29 +645,6 @@ describe('URL utility', () => {
});
});
- describe('urlParamsToObject', () => {
- it('parses path for label with trailing +', () => {
- // eslint-disable-next-line import/no-deprecated
- expect(urlUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({
- label_name: ['label+'],
- });
- });
-
- it('parses path for milestone with trailing +', () => {
- // eslint-disable-next-line import/no-deprecated
- expect(urlUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({
- milestone_title: 'A+',
- });
- });
-
- it('parses path for search terms with spaces', () => {
- // eslint-disable-next-line import/no-deprecated
- expect(urlUtils.urlParamsToObject('search=two+words', {})).toEqual({
- search: 'two words',
- });
- });
- });
-
describe('queryToObject', () => {
it.each`
case | query | options | result
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index ea9eb7bf923..1dc913e5c78 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -99,10 +99,14 @@ describe('LeaveModal', () => {
});
});
- it("does NOT display oncall schedules list when member's user is NOT a part of on-call schedules ", () => {
+ it("does NOT display oncall schedules list when member's user is NOT a part of on-call schedules ", async () => {
+ wrapper.destroy();
+
const memberWithoutOncallSchedules = cloneDeep(member);
- delete (memberWithoutOncallSchedules, 'user.oncallSchedules');
+ delete memberWithoutOncallSchedules.user.oncallSchedules;
createComponent({ member: memberWithoutOncallSchedules });
+ await nextTick();
+
expect(findOncallSchedulesList().exists()).toBe(false);
});
});
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 23e9bf8b447..ced9b71125b 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -34,6 +34,44 @@ describe('MergeRequestTabs', () => {
gl.mrWidget = {};
});
+ describe('clickTab', () => {
+ let params;
+
+ beforeEach(() => {
+ document.documentElement.scrollTop = 100;
+
+ params = {
+ metaKey: false,
+ ctrlKey: false,
+ which: 1,
+ stopImmediatePropagation() {},
+ preventDefault() {},
+ currentTarget: {
+ getAttribute(attr) {
+ return attr === 'href' ? 'a/tab/url' : null;
+ },
+ },
+ };
+ });
+
+ it("stores the current scroll position if there's an active tab", () => {
+ testContext.class.currentTab = 'someTab';
+
+ testContext.class.clickTab(params);
+
+ expect(testContext.class.scrollPositions.someTab).toBe(100);
+ });
+
+ it("doesn't store a scroll position if there's no active tab", () => {
+ // this happens on first load, and we just don't want to store empty values in the `null` property
+ testContext.class.currentTab = null;
+
+ testContext.class.clickTab(params);
+
+ expect(testContext.class.scrollPositions).toEqual({});
+ });
+ });
+
describe('opensInNewTab', () => {
const windowTarget = '_blank';
let clickTabParams;
@@ -258,6 +296,7 @@ describe('MergeRequestTabs', () => {
beforeEach(() => {
jest.spyOn(mainContent, 'getBoundingClientRect').mockReturnValue({ top: 10 });
jest.spyOn(tabContent, 'getBoundingClientRect').mockReturnValue({ top: 100 });
+ jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
jest.spyOn(document, 'querySelector').mockImplementation((selector) => {
return selector === '.content-wrapper' ? mainContent : tabContent;
});
@@ -267,8 +306,6 @@ describe('MergeRequestTabs', () => {
it('calls window scrollTo with options if document has scrollBehavior', () => {
document.documentElement.style.scrollBehavior = '';
- jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
-
testContext.class.tabShown('commits', 'foobar');
expect(window.scrollTo.mock.calls[0][0]).toEqual({ top: 39, behavior: 'smooth' });
@@ -276,11 +313,50 @@ describe('MergeRequestTabs', () => {
it('calls window scrollTo with two args if document does not have scrollBehavior', () => {
jest.spyOn(document.documentElement, 'style', 'get').mockReturnValue({});
- jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
testContext.class.tabShown('commits', 'foobar');
expect(window.scrollTo.mock.calls[0]).toEqual([0, 39]);
});
+
+ describe('when switching tabs', () => {
+ const SCROLL_TOP = 100;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ beforeEach(() => {
+ jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
+ testContext.class.mergeRequestTabs = document.createElement('div');
+ testContext.class.mergeRequestTabPanes = document.createElement('div');
+ testContext.class.currentTab = 'tab';
+ testContext.class.scrollPositions = { newTab: SCROLL_TOP };
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('scrolls to the stored position, if one is stored', () => {
+ testContext.class.tabShown('newTab');
+
+ jest.advanceTimersByTime(250);
+
+ expect(window.scrollTo.mock.calls[0][0]).toEqual({
+ top: SCROLL_TOP,
+ left: 0,
+ behavior: 'auto',
+ });
+ });
+
+ it('scrolls to 0, if no position is stored', () => {
+ testContext.class.tabShown('unknownTab');
+
+ jest.advanceTimersByTime(250);
+
+ expect(window.scrollTo.mock.calls[0][0]).toEqual({ top: 0, left: 0, behavior: 'auto' });
+ });
+ });
});
});
diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js
index 91b2acf23c5..a53d6ca5de1 100644
--- a/spec/frontend/milestones/stores/mutations_spec.js
+++ b/spec/frontend/milestones/stores/mutations_spec.js
@@ -174,6 +174,35 @@ describe('Milestones combobox Vuex store mutations', () => {
});
});
+ it('falls back to the length of list if pagination headers are missing', () => {
+ const response = {
+ data: [
+ {
+ title: 'v0.1',
+ },
+ {
+ title: 'v0.2',
+ },
+ ],
+ headers: {},
+ };
+
+ mutations[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response);
+
+ expect(state.matches.projectMilestones).toEqual({
+ list: [
+ {
+ title: 'v0.1',
+ },
+ {
+ title: 'v0.2',
+ },
+ ],
+ error: null,
+ totalCount: 2,
+ });
+ });
+
describe(`${types.RECEIVE_PROJECT_MILESTONES_ERROR}`, () => {
it('updates state.matches.projectMilestones to an empty state with the error object', () => {
const error = new Error('Something went wrong!');
@@ -227,6 +256,35 @@ describe('Milestones combobox Vuex store mutations', () => {
});
});
+ it('falls back to the length of data received if pagination headers are missing', () => {
+ const response = {
+ data: [
+ {
+ title: 'group-0.1',
+ },
+ {
+ title: 'group-0.2',
+ },
+ ],
+ headers: {},
+ };
+
+ mutations[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response);
+
+ expect(state.matches.groupMilestones).toEqual({
+ list: [
+ {
+ title: 'group-0.1',
+ },
+ {
+ title: 'group-0.2',
+ },
+ ],
+ error: null,
+ totalCount: 2,
+ });
+ });
+
describe(`${types.RECEIVE_GROUP_MILESTONES_ERROR}`, () => {
it('updates state.matches.groupMilestones to an empty state with the error object', () => {
const error = new Error('Something went wrong!');
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 08f9e07244f..05538dbaeee 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -36,11 +36,15 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<gl-dropdown-stub
category="primary"
class="flex-grow-1"
+ clearalltext="Clear all"
data-qa-selector="environments_dropdown"
headertext=""
hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
id="monitor-environments-dropdown"
menu-class="monitor-environment-dropdown-menu"
+ showhighlighteditemstitle="true"
size="medium"
text="production"
toggleclass="dropdown-menu-toggle"
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index deeee5d6589..707efa21528 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -1,3 +1,4 @@
+import { mount } from '@vue/test-utils';
import katex from 'katex';
import Vue from 'vue';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
@@ -6,6 +7,28 @@ const Component = Vue.extend(MarkdownComponent);
window.katex = katex;
+function buildCellComponent(cell, relativePath = '') {
+ return mount(Component, {
+ propsData: {
+ cell,
+ },
+ provide: {
+ relativeRawPath: relativePath,
+ },
+ }).vm;
+}
+
+function buildMarkdownComponent(markdownContent, relativePath = '') {
+ return buildCellComponent(
+ {
+ cell_type: 'markdown',
+ metadata: {},
+ source: markdownContent,
+ },
+ relativePath,
+ );
+}
+
describe('Markdown component', () => {
let vm;
let cell;
@@ -17,12 +40,7 @@ describe('Markdown component', () => {
// eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
- vm = new Component({
- propsData: {
- cell,
- },
- });
- vm.$mount();
+ vm = buildCellComponent(cell);
return vm.$nextTick();
});
@@ -61,17 +79,36 @@ describe('Markdown component', () => {
expect(findLink().getAttribute('data-type')).toBe(null);
});
+ describe('When parsing images', () => {
+ it.each([
+ [
+ 'for relative images in root folder, it does',
+ '![](local_image.png)\n',
+ 'src="/raw/local_image',
+ ],
+ [
+ 'for relative images in child folders, it does',
+ '![](data/local_image.png)\n',
+ 'src="/raw/data',
+ ],
+ ["for embedded images, it doesn't", '![](data:image/jpeg;base64)\n', 'src="data:'],
+ ["for images urls, it doesn't", '![](http://image.png)\n', 'src="http:'],
+ ])('%s', async ([testMd, mustContain]) => {
+ vm = buildMarkdownComponent([testMd], '/raw/');
+
+ await vm.$nextTick();
+
+ expect(vm.$el.innerHTML).toContain(mustContain);
+ });
+ });
+
describe('tables', () => {
beforeEach(() => {
json = getJSONFixture('blob/notebook/markdown-table.json');
});
it('renders images and text', () => {
- vm = new Component({
- propsData: {
- cell: json.cells[0],
- },
- }).$mount();
+ vm = buildCellComponent(json.cells[0]);
return vm.$nextTick().then(() => {
const images = vm.$el.querySelectorAll('img');
@@ -102,48 +139,28 @@ describe('Markdown component', () => {
});
it('renders multi-line katex', async () => {
- vm = new Component({
- propsData: {
- cell: json.cells[0],
- },
- }).$mount();
+ vm = buildCellComponent(json.cells[0]);
await vm.$nextTick();
expect(vm.$el.querySelector('.katex')).not.toBeNull();
});
it('renders inline katex', async () => {
- vm = new Component({
- propsData: {
- cell: json.cells[1],
- },
- }).$mount();
+ vm = buildCellComponent(json.cells[1]);
await vm.$nextTick();
expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
});
it('renders multiple inline katex', async () => {
- vm = new Component({
- propsData: {
- cell: json.cells[1],
- },
- }).$mount();
+ vm = buildCellComponent(json.cells[1]);
await vm.$nextTick();
expect(vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
});
it('output cell in case of katex error', async () => {
- vm = new Component({
- propsData: {
- cell: {
- cell_type: 'markdown',
- metadata: {},
- source: ['Some invalid $a & b$ inline formula $b & c$\n', '\n'],
- },
- },
- }).$mount();
+ vm = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
await vm.$nextTick();
// expect one paragraph with no katex formula in it
@@ -152,15 +169,10 @@ describe('Markdown component', () => {
});
it('output cell and render remaining formula in case of katex error', async () => {
- vm = new Component({
- propsData: {
- cell: {
- cell_type: 'markdown',
- metadata: {},
- source: ['An invalid $a & b$ inline formula and a vaild one $b = c$\n', '\n'],
- },
- },
- }).$mount();
+ vm = buildMarkdownComponent([
+ 'An invalid $a & b$ inline formula and a vaild one $b = c$\n',
+ '\n',
+ ]);
await vm.$nextTick();
// expect one paragraph with no katex formula in it
@@ -169,15 +181,7 @@ describe('Markdown component', () => {
});
it('renders math formula in list object', async () => {
- vm = new Component({
- propsData: {
- cell: {
- cell_type: 'markdown',
- metadata: {},
- source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'],
- },
- },
- }).$mount();
+ vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
await vm.$nextTick();
// expect one list with a katex formula in it
@@ -186,15 +190,7 @@ describe('Markdown component', () => {
});
it("renders math formula with tick ' in it", async () => {
- vm = new Component({
- propsData: {
- cell: {
- cell_type: 'markdown',
- metadata: {},
- source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'],
- },
- },
- }).$mount();
+ vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
await vm.$nextTick();
// expect one list with a katex formula in it
@@ -203,15 +199,7 @@ describe('Markdown component', () => {
});
it('renders math formula with less-than-operator < in it', async () => {
- vm = new Component({
- propsData: {
- cell: {
- cell_type: 'markdown',
- metadata: {},
- source: ['- list with inline $a=2$ inline formula $a + b < c$\n', '\n'],
- },
- },
- }).$mount();
+ vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b < c$\n', '\n']);
await vm.$nextTick();
// expect one list with a katex formula in it
@@ -220,15 +208,7 @@ describe('Markdown component', () => {
});
it('renders math formula with greater-than-operator > in it', async () => {
- vm = new Component({
- propsData: {
- cell: {
- cell_type: 'markdown',
- metadata: {},
- source: ['- list with inline $a=2$ inline formula $a + b > c$\n', '\n'],
- },
- },
- }).$mount();
+ vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b > c$\n', '\n']);
await vm.$nextTick();
// expect one list with a katex formula in it
diff --git a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js
index 0b585ab860b..803ac4a219d 100644
--- a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js
+++ b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js
@@ -90,7 +90,8 @@ export default [
' </g>\n',
'</svg>',
].join(),
- output: '<svg height="115.02pt" id="svg2"',
+ output:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="388.84pt" version="1.0" id="svg2" height="115.02pt">',
},
],
];
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index 945af08e4d5..4d0dacaf37e 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -1,3 +1,4 @@
+import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Notebook from '~/notebook/index.vue';
@@ -13,14 +14,16 @@ describe('Notebook component', () => {
jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json');
});
+ function buildComponent(notebook) {
+ return mount(Component, {
+ propsData: { notebook, codeCssClass: 'js-code-class' },
+ provide: { relativeRawPath: '' },
+ }).vm;
+ }
+
describe('without JSON', () => {
beforeEach((done) => {
- vm = new Component({
- propsData: {
- notebook: {},
- },
- });
- vm.$mount();
+ vm = buildComponent({});
setImmediate(() => {
done();
@@ -34,13 +37,7 @@ describe('Notebook component', () => {
describe('with JSON', () => {
beforeEach((done) => {
- vm = new Component({
- propsData: {
- notebook: json,
- codeCssClass: 'js-code-class',
- },
- });
- vm.$mount();
+ vm = buildComponent(json);
setImmediate(() => {
done();
@@ -66,13 +63,7 @@ describe('Notebook component', () => {
describe('with worksheets', () => {
beforeEach((done) => {
- vm = new Component({
- propsData: {
- notebook: jsonWithWorksheet,
- codeCssClass: 'js-code-class',
- },
- });
- vm.$mount();
+ vm = buildComponent(jsonWithWorksheet);
setImmediate(() => {
done();
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index bb79b43205b..c3a51c51de0 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -10,6 +10,7 @@ import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import CommentForm from '~/notes/components/comment_form.vue';
+import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
import * as constants from '~/notes/constants';
import eventHub from '~/notes/event_hub';
import { COMMENT_FORM } from '~/notes/i18n';
@@ -33,8 +34,8 @@ describe('issue_comment_form component', () => {
const findAddToReviewButton = () => wrapper.findByTestId('add-to-review-button');
const findAddCommentNowButton = () => wrapper.findByTestId('add-comment-now-button');
const findConfidentialNoteCheckbox = () => wrapper.findByTestId('confidential-note-checkbox');
- const findCommentGlDropdown = () => wrapper.findByTestId('comment-button');
- const findCommentButton = () => findCommentGlDropdown().find('button');
+ const findCommentTypeDropdown = () => wrapper.findComponent(CommentTypeDropdown);
+ const findCommentButton = () => findCommentTypeDropdown().find('button');
const findErrorAlerts = () => wrapper.findAllComponents(GlAlert).wrappers;
async function clickCommentButton({ waitForComponent = true, waitForNetwork = true } = {}) {
@@ -381,7 +382,7 @@ describe('issue_comment_form component', () => {
it('should render comment button as disabled', () => {
mountComponent();
- expect(findCommentGlDropdown().props('disabled')).toBe(true);
+ expect(findCommentTypeDropdown().props('disabled')).toBe(true);
});
it('should enable comment button if it has note', async () => {
@@ -389,7 +390,7 @@ describe('issue_comment_form component', () => {
await wrapper.setData({ note: 'Foo' });
- expect(findCommentGlDropdown().props('disabled')).toBe(false);
+ expect(findCommentTypeDropdown().props('disabled')).toBe(false);
});
it('should update buttons texts when it has note', () => {
@@ -624,7 +625,7 @@ describe('issue_comment_form component', () => {
it('when no drafts exist, should not render', () => {
mountComponent();
- expect(findCommentGlDropdown().exists()).toBe(true);
+ expect(findCommentTypeDropdown().exists()).toBe(true);
expect(findAddToReviewButton().exists()).toBe(false);
expect(findAddCommentNowButton().exists()).toBe(false);
});
@@ -637,7 +638,7 @@ describe('issue_comment_form component', () => {
it('should render', () => {
mountComponent();
- expect(findCommentGlDropdown().exists()).toBe(false);
+ expect(findCommentTypeDropdown().exists()).toBe(false);
expect(findAddToReviewButton().exists()).toBe(true);
expect(findAddCommentNowButton().exists()).toBe(true);
});
diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js
new file mode 100644
index 00000000000..5e1cb813369
--- /dev/null
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -0,0 +1,64 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
+import * as constants from '~/notes/constants';
+import { COMMENT_FORM } from '~/notes/i18n';
+
+describe('CommentTypeDropdown component', () => {
+ let wrapper;
+
+ const findCommentGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findCommentDropdownOption = () => wrapper.findAllComponents(GlDropdownItem).at(0);
+ const findDiscussionDropdownOption = () => wrapper.findAllComponents(GlDropdownItem).at(1);
+
+ const mountComponent = ({ props = {} } = {}) => {
+ wrapper = extendedWrapper(
+ mount(CommentTypeDropdown, {
+ propsData: {
+ noteableDisplayName: 'issue',
+ noteType: constants.COMMENT,
+ ...props,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Should label action button "Comment" and correct dropdown item checked when selected', () => {
+ mountComponent({ props: { noteType: constants.COMMENT } });
+
+ expect(findCommentGlDropdown().props()).toMatchObject({ text: COMMENT_FORM.comment });
+ expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: true });
+ expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: false });
+ });
+
+ it('Should label action button "Start Thread" and correct dropdown item option checked when selected', () => {
+ mountComponent({ props: { noteType: constants.DISCUSSION } });
+
+ expect(findCommentGlDropdown().props()).toMatchObject({ text: COMMENT_FORM.startThread });
+ expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: false });
+ expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: true });
+ });
+
+ it('Should emit `change` event when clicking on an alternate dropdown option', () => {
+ mountComponent({ props: { noteType: constants.DISCUSSION } });
+
+ findCommentDropdownOption().vm.$emit('click');
+ findDiscussionDropdownOption().vm.$emit('click');
+
+ expect(wrapper.emitted('change')[0]).toEqual([constants.COMMENT]);
+ expect(wrapper.emitted('change').length).toEqual(1);
+ });
+
+ it('Should emit `click` event when clicking on the action button', () => {
+ mountComponent({ props: { noteType: constants.DISCUSSION } });
+
+ findCommentGlDropdown().vm.$emit('click');
+
+ expect(wrapper.emitted('click').length > 0).toBe(true);
+ });
+});
diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 0cf43b8fd97..34623f8aa13 100644
--- a/spec/frontend/notes/old_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -14,9 +14,8 @@ import * as urlUtility from '~/lib/utils/url_utility';
window.jQuery = $;
require('autosize');
require('~/commons');
-require('~/notes');
+const Notes = require('~/deprecated_notes').default;
-const { Notes } = window;
const FLASH_TYPE_ALERT = 'alert';
const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
const fixture = 'snippets/show.html';
@@ -31,7 +30,7 @@ gl.utils.disableButtonIfEmptyField = () => {};
// the following test is unreliable and failing in main 2-3 times a day
// see https://gitlab.com/gitlab-org/gitlab/issues/206906#note_290602581
// eslint-disable-next-line jest/no-disabled-tests
-describe.skip('Old Notes (~/notes.js)', () => {
+describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
loadFixtures(fixture);
@@ -67,7 +66,7 @@ describe.skip('Old Notes (~/notes.js)', () => {
it('calls postComment when comment button is clicked', () => {
jest.spyOn(Notes.prototype, 'postComment');
- new window.Notes('', []);
+ new Notes('', []);
$('.js-comment-button').click();
expect(Notes.prototype.postComment).toHaveBeenCalled();
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
index 45d261625b4..451cf743e35 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
@@ -177,15 +177,6 @@ exports[`PackageTitle renders without tags 1`] = `
texttooltip=""
/>
</div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <package-tags-stub
- hidelabel="true"
- tagdisplaylimit="2"
- tags="[object Object],[object Object],[object Object]"
- />
- </div>
</div>
</div>
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 0504a42dfcf..7a71a1cea0f 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,10 +1,11 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
conanMetadata,
mavenMetadata,
nugetMetadata,
packageData,
+ composerMetadata,
+ pypiMetadata,
} from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import {
@@ -12,12 +13,15 @@ import {
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
+ PACKAGE_TYPE_COMPOSER,
+ PACKAGE_TYPE_PYPI,
} 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() };
const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() };
const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
+const composerPackage = { packageType: PACKAGE_TYPE_COMPOSER, metadata: composerMetadata() };
+const pypiPackage = { packageType: PACKAGE_TYPE_PYPI, metadata: pypiMetadata() };
const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} };
describe('Package Additional Metadata', () => {
@@ -32,8 +36,7 @@ describe('Package Additional Metadata', () => {
wrapper = shallowMountExtended(component, {
propsData: { ...defaultProps, ...props },
stubs: {
- DetailsRow,
- GlSprintf,
+ component: { template: '<div data-testid="component-is"></div>' },
},
});
};
@@ -45,12 +48,7 @@ describe('Package Additional Metadata', () => {
const findTitle = () => wrapper.findByTestId('title');
const findMainArea = () => wrapper.findByTestId('main');
- const findNugetSource = () => wrapper.findByTestId('nuget-source');
- const findNugetLicense = () => wrapper.findByTestId('nuget-license');
- const findConanRecipe = () => wrapper.findByTestId('conan-recipe');
- const findMavenApp = () => wrapper.findByTestId('maven-app');
- const findMavenGroup = () => wrapper.findByTestId('maven-group');
- const findElementLink = (container) => container.findComponent(GlLink);
+ const findComponentIs = () => wrapper.findByTestId('component-is');
it('has the correct title', () => {
mountComponent();
@@ -62,11 +60,13 @@ describe('Package Additional Metadata', () => {
});
it.each`
- packageEntity | visible | packageType
- ${mavenPackage} | ${true} | ${PACKAGE_TYPE_MAVEN}
- ${conanPackage} | ${true} | ${PACKAGE_TYPE_CONAN}
- ${nugetPackage} | ${true} | ${PACKAGE_TYPE_NUGET}
- ${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM}
+ packageEntity | visible | packageType
+ ${mavenPackage} | ${true} | ${PACKAGE_TYPE_MAVEN}
+ ${conanPackage} | ${true} | ${PACKAGE_TYPE_CONAN}
+ ${nugetPackage} | ${true} | ${PACKAGE_TYPE_NUGET}
+ ${composerPackage} | ${true} | ${PACKAGE_TYPE_COMPOSER}
+ ${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 }) => {
@@ -74,57 +74,11 @@ describe('Package Additional Metadata', () => {
expect(findTitle().exists()).toBe(visible);
expect(findMainArea().exists()).toBe(visible);
+ expect(findComponentIs().exists()).toBe(visible);
+
+ if (visible) {
+ expect(findComponentIs().props('packageEntity')).toEqual(packageEntity);
+ }
},
);
-
- describe('nuget metadata', () => {
- beforeEach(() => {
- mountComponent({ packageEntity: nugetPackage });
- });
-
- it.each`
- name | finderFunction | text | link | icon
- ${'source'} | ${findNugetSource} | ${'Source project located at projectUrl'} | ${'projectUrl'} | ${'project'}
- ${'license'} | ${findNugetLicense} | ${'License information located at licenseUrl'} | ${'licenseUrl'} | ${'license'}
- `('$name element', ({ finderFunction, text, link, icon }) => {
- const element = finderFunction();
- expect(element.exists()).toBe(true);
- expect(element.text()).toBe(text);
- expect(element.props('icon')).toBe(icon);
- expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]);
- });
- });
-
- describe('conan metadata', () => {
- beforeEach(() => {
- mountComponent({ packageEntity: conanPackage });
- });
-
- it.each`
- name | finderFunction | text | icon
- ${'recipe'} | ${findConanRecipe} | ${'Recipe: package-8/1.0.0@gitlab-org+gitlab-test/stable'} | ${'information-o'}
- `('$name element', ({ finderFunction, text, icon }) => {
- const element = finderFunction();
- expect(element.exists()).toBe(true);
- expect(element.text()).toBe(text);
- expect(element.props('icon')).toBe(icon);
- });
- });
-
- describe('maven metadata', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it.each`
- name | finderFunction | text | icon
- ${'app'} | ${findMavenApp} | ${'App name: appName'} | ${'information-o'}
- ${'group'} | ${findMavenGroup} | ${'App group: appGroup'} | ${'information-o'}
- `('$name element', ({ finderFunction, text, icon }) => {
- const element = finderFunction();
- expect(element.exists()).toBe(true);
- expect(element.text()).toBe(text);
- expect(element.props('icon')).toBe(icon);
- });
- });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
new file mode 100644
index 00000000000..e744680cb9a
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
@@ -0,0 +1,58 @@
+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 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) },
+ stubs: {
+ DetailsRow,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findComposerTargetSha = () => wrapper.findByTestId('composer-target-sha');
+ const findComposerTargetShaCopyButton = () => wrapper.findComponent(ClipboardButton);
+ const findComposerJson = () => wrapper.findByTestId('composer-json');
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it.each`
+ name | finderFunction | text | icon
+ ${'target-sha'} | ${findComposerTargetSha} | ${'Target SHA: b83d6e391c22777fca1ed3012fce84f633d7fed0'} | ${'information-o'}
+ ${'composer-json'} | ${findComposerJson} | ${'Composer.json with license: MIT and version: 1.0.0'} | ${'information-o'}
+ `('$name element', ({ finderFunction, text, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ });
+
+ it('target-sha has a copy button', () => {
+ expect(findComposerTargetShaCopyButton().exists()).toBe(true);
+ expect(findComposerTargetShaCopyButton().props()).toMatchObject({
+ text: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+ title: 'Copy target SHA',
+ });
+ });
+});
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
new file mode 100644
index 00000000000..46593047f1f
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
@@ -0,0 +1,48 @@
+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 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),
+ },
+ stubs: {
+ DetailsRow,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findConanRecipe = () => wrapper.findByTestId('conan-recipe');
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it.each`
+ name | finderFunction | text | icon
+ ${'recipe'} | ${findConanRecipe} | ${'Recipe: package-8/1.0.0@gitlab-org+gitlab-test/stable'} | ${'information-o'}
+ `('$name element', ({ finderFunction, text, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ });
+});
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
new file mode 100644
index 00000000000..bc54cf1cb98
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
@@ -0,0 +1,52 @@
+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 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),
+ },
+ },
+ stubs: {
+ DetailsRow,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findMavenApp = () => wrapper.findByTestId('maven-app');
+ const findMavenGroup = () => wrapper.findByTestId('maven-group');
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it.each`
+ name | finderFunction | text | icon
+ ${'app'} | ${findMavenApp} | ${'App name: appName'} | ${'information-o'}
+ ${'group'} | ${findMavenGroup} | ${'App group: appGroup'} | ${'information-o'}
+ `('$name element', ({ finderFunction, text, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
new file mode 100644
index 00000000000..279900edff2
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
@@ -0,0 +1,55 @@
+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 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';
+
+const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
+
+describe('Nuget Metadata', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMountExtended(component, {
+ propsData: {
+ packageEntity: {
+ ...packageData(nugetPackage),
+ },
+ },
+ stubs: {
+ DetailsRow,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findNugetSource = () => wrapper.findByTestId('nuget-source');
+ const findNugetLicense = () => wrapper.findByTestId('nuget-license');
+ const findElementLink = (container) => container.findComponent(GlLink);
+
+ beforeEach(() => {
+ mountComponent({ packageEntity: nugetPackage });
+ });
+
+ it.each`
+ name | finderFunction | text | link | icon
+ ${'source'} | ${findNugetSource} | ${'Source project located at projectUrl'} | ${'projectUrl'} | ${'project'}
+ ${'license'} | ${findNugetLicense} | ${'License information located at licenseUrl'} | ${'licenseUrl'} | ${'license'}
+ `('$name element', ({ finderFunction, text, link, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]);
+ });
+});
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
new file mode 100644
index 00000000000..c4481c3f20b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
@@ -0,0 +1,48 @@
+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 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),
+ },
+ },
+ stubs: {
+ DetailsRow,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findPypiRequiredPython = () => wrapper.findByTestId('pypi-required-python');
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it.each`
+ name | finderFunction | text | icon
+ ${'pypi-required-python'} | ${findPypiRequiredPython} | ${'Required Python: 1.0.0'} | ${'information-o'}
+ `('$name element', ({ finderFunction, text, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
index 327f6d81905..d59c3184e4e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
@@ -30,6 +31,9 @@ describe('PackageTitle', () => {
TitleArea,
GlSprintf,
},
+ directives: {
+ GlResizeObserver: createMockDirective(),
+ },
});
return wrapper.vm.$nextTick();
}
@@ -51,7 +55,7 @@ describe('PackageTitle', () => {
describe('renders', () => {
it('without tags', async () => {
- await createComponent();
+ await createComponent({ ...packageData(), packageFiles: { nodes: packageFiles() } });
expect(wrapper.element).toMatchSnapshot();
});
@@ -64,12 +68,26 @@ describe('PackageTitle', () => {
it('with tags on mobile', async () => {
jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false);
+
await createComponent();
await wrapper.vm.$nextTick();
expect(findPackageBadges()).toHaveLength(packageTags().length);
});
+
+ it('when the page is resized', async () => {
+ await createComponent();
+
+ expect(findPackageBadges()).toHaveLength(0);
+
+ jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false);
+ const { value } = getBinding(wrapper.element, 'gl-resize-observer');
+ value();
+
+ await wrapper.vm.$nextTick();
+ expect(findPackageBadges()).toHaveLength(packageTags().length);
+ });
});
describe('package title', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap
new file mode 100644
index 00000000000..dbebdeeb452
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/packages_list_app_spec.js.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`packages_list_app renders 1`] = `
+<div>
+ <div
+ help-url="foo"
+ />
+
+ <div />
+
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt=""
+ class="gl-max-w-full"
+ role="img"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-justify-content-center"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js
new file mode 100644
index 00000000000..6c871a34d50
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_app_spec.js
@@ -0,0 +1,273 @@
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import createFlash from '~/flash';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
+import PackageListApp from '~/packages_and_registries/package_registry/components/list/packages_list_app.vue';
+import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import * as packageUtils from '~/packages_and_registries/shared/utils';
+
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('packages_list_app', () => {
+ let wrapper;
+ let store;
+
+ const PackageList = {
+ name: 'package-list',
+ template: '<div><slot name="empty-state"></slot></div>',
+ };
+ const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
+
+ // we need to manually stub dynamic imported components because shallowMount is not able to stub them automatically. See: https://github.com/vuejs/vue-test-utils/issues/1279
+ const PackageSearch = { name: 'PackageSearch', template: '<div></div>' };
+ const PackageTitle = { name: 'PackageTitle', template: '<div></div>' };
+ const InfrastructureTitle = { name: 'InfrastructureTitle', template: '<div></div>' };
+ const InfrastructureSearch = { name: 'InfrastructureSearch', template: '<div></div>' };
+
+ const emptyListHelpUrl = 'helpUrl';
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findListComponent = () => wrapper.find(PackageList);
+ const findPackageSearch = () => wrapper.find(PackageSearch);
+ const findPackageTitle = () => wrapper.find(PackageTitle);
+ const findInfrastructureTitle = () => wrapper.find(InfrastructureTitle);
+ const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
+
+ const createStore = (filter = []) => {
+ store = new Vuex.Store({
+ state: {
+ isLoading: false,
+ config: {
+ resourceId: 'project_id',
+ emptyListIllustration: 'helpSvg',
+ emptyListHelpUrl,
+ packageHelpUrl: 'foo',
+ },
+ filter,
+ },
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = (provide) => {
+ wrapper = shallowMount(PackageListApp, {
+ localVue,
+ store,
+ stubs: {
+ GlEmptyState,
+ GlLoadingIcon,
+ PackageList,
+ GlSprintf,
+ GlLink,
+ PackageSearch,
+ PackageTitle,
+ InfrastructureTitle,
+ InfrastructureSearch,
+ },
+ provide,
+ });
+ };
+
+ beforeEach(() => {
+ createStore();
+ jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders', () => {
+ mountComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('call requestPackagesList on page:changed', () => {
+ mountComponent();
+ store.dispatch.mockClear();
+
+ const list = findListComponent();
+ list.vm.$emit('page:changed', 1);
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
+ });
+
+ it('call requestDeletePackage on package:delete', () => {
+ mountComponent();
+
+ const list = findListComponent();
+ list.vm.$emit('package:delete', 'foo');
+ expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
+ });
+
+ it('does call requestPackagesList only one time on render', () => {
+ mountComponent();
+
+ expect(store.dispatch).toHaveBeenCalledTimes(3);
+ expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object));
+ expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array));
+ expect(store.dispatch).toHaveBeenNthCalledWith(3, 'requestPackagesList');
+ });
+
+ describe('url query string handling', () => {
+ const defaultQueryParamsMock = {
+ search: [1, 2],
+ type: 'npm',
+ sort: 'asc',
+ orderBy: 'created',
+ };
+
+ it('calls setSorting with the query string based sorting', () => {
+ jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
+
+ mountComponent();
+
+ expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', {
+ orderBy: defaultQueryParamsMock.orderBy,
+ sort: defaultQueryParamsMock.sort,
+ });
+ });
+
+ it('calls setFilter with the query string based filters', () => {
+ jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
+
+ mountComponent();
+
+ expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
+ { type: 'type', value: { data: defaultQueryParamsMock.type } },
+ { type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[0] } },
+ { type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[1] } },
+ ]);
+ });
+
+ it('calls setSorting and setFilters with the results of extractFilterAndSorting', () => {
+ jest
+ .spyOn(packageUtils, 'extractFilterAndSorting')
+ .mockReturnValue({ filters: ['foo'], sorting: { sort: 'desc' } });
+
+ mountComponent();
+
+ expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { sort: 'desc' });
+ expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', ['foo']);
+ });
+ });
+
+ describe('empty state', () => {
+ it('generate the correct empty list link', () => {
+ mountComponent();
+
+ const link = findListComponent().find(GlLink);
+
+ expect(link.attributes('href')).toBe(emptyListHelpUrl);
+ expect(link.text()).toBe('publish and share your packages');
+ });
+
+ it('includes the right content on the default tab', () => {
+ mountComponent();
+
+ const heading = findEmptyState().find('h1');
+
+ expect(heading.text()).toBe('There are no packages yet');
+ });
+ });
+
+ describe('filter without results', () => {
+ beforeEach(() => {
+ createStore([{ type: 'something' }]);
+ mountComponent();
+ });
+
+ it('should show specific empty message', () => {
+ expect(findEmptyState().text()).toContain('Sorry, your filter produced no results');
+ expect(findEmptyState().text()).toContain(
+ 'To widen your search, change or remove the filters above',
+ );
+ });
+ });
+
+ describe('Package Search', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findPackageSearch().exists()).toBe(true);
+ });
+
+ it('on update fetches data from the store', () => {
+ mountComponent();
+ store.dispatch.mockClear();
+
+ findPackageSearch().vm.$emit('update');
+
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
+ });
+
+ describe('Infrastructure config', () => {
+ it('defaults to package registry components', () => {
+ mountComponent();
+
+ expect(findPackageSearch().exists()).toBe(true);
+ expect(findPackageTitle().exists()).toBe(true);
+
+ expect(findInfrastructureTitle().exists()).toBe(false);
+ expect(findInfrastructureSearch().exists()).toBe(false);
+ });
+
+ it('mount different component based on the provided values', () => {
+ mountComponent({
+ titleComponent: 'InfrastructureTitle',
+ searchComponent: 'InfrastructureSearch',
+ });
+
+ expect(findPackageSearch().exists()).toBe(false);
+ expect(findPackageTitle().exists()).toBe(false);
+
+ expect(findInfrastructureTitle().exists()).toBe(true);
+ expect(findInfrastructureSearch().exists()).toBe(true);
+ });
+ });
+
+ describe('delete alert handling', () => {
+ const originalLocation = window.location.href;
+ const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
+
+ beforeEach(() => {
+ createStore();
+ jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
+ setWindowLocation(search);
+ });
+
+ afterEach(() => {
+ setWindowLocation(originalLocation);
+ });
+
+ it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
+ mountComponent();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_SUCCESS_MESSAGE,
+ type: 'notice',
+ });
+ });
+
+ it('calls historyReplaceState with a clean url', () => {
+ mountComponent();
+
+ expect(commonUtils.historyReplaceState).toHaveBeenCalledWith(originalLocation);
+ });
+
+ it(`does nothing if the query string does not contain ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
+ setWindowLocation('?');
+ mountComponent();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(commonUtils.historyReplaceState).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
new file mode 100644
index 00000000000..b624e66482d
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -0,0 +1,217 @@
+import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { last } from 'lodash';
+import Vuex from 'vuex';
+import stubChildren from 'helpers/stub_children';
+import { packageList } from 'jest/packages/mock_data';
+import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import { TrackingActions } from '~/packages/shared/constants';
+import * as SharedUtils from '~/packages/shared/utils';
+import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
+import Tracking from '~/tracking';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('packages_list', () => {
+ let wrapper;
+ let store;
+
+ const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
+
+ const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
+ const findPackageListPagination = () => wrapper.find(GlPagination);
+ const findPackageListDeleteModal = () => wrapper.find(GlModal);
+ const findEmptySlot = () => wrapper.find(EmptySlotStub);
+ const findPackagesListRow = () => wrapper.find(PackagesListRow);
+
+ const createStore = (isGroupPage, packages, isLoading) => {
+ const state = {
+ isLoading,
+ packages,
+ pagination: {
+ perPage: 1,
+ total: 1,
+ page: 1,
+ },
+ config: {
+ isGroupPage,
+ },
+ sorting: {
+ orderBy: 'version',
+ sort: 'desc',
+ },
+ };
+ store = new Vuex.Store({
+ state,
+ getters: {
+ getList: () => packages,
+ },
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = ({
+ isGroupPage = false,
+ packages = packageList,
+ isLoading = false,
+ ...options
+ } = {}) => {
+ createStore(isGroupPage, packages, isLoading);
+
+ wrapper = mount(PackagesList, {
+ localVue,
+ store,
+ stubs: {
+ ...stubChildren(PackagesList),
+ GlTable,
+ GlModal,
+ },
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when is loading', () => {
+ beforeEach(() => {
+ mountComponent({
+ packages: [],
+ isLoading: true,
+ });
+ });
+
+ it('shows skeleton loader when loading', () => {
+ expect(findPackagesListLoader().exists()).toBe(true);
+ });
+ });
+
+ describe('when is not loading', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('does not show skeleton loader when not loading', () => {
+ expect(findPackagesListLoader().exists()).toBe(false);
+ });
+ });
+
+ describe('layout', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('contains a pagination component', () => {
+ const sorting = findPackageListPagination();
+ expect(sorting.exists()).toBe(true);
+ });
+
+ it('contains a modal component', () => {
+ const sorting = findPackageListDeleteModal();
+ expect(sorting.exists()).toBe(true);
+ });
+ });
+
+ describe('when the user can destroy the package', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => {
+ const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
+ const item = last(wrapper.vm.list);
+
+ findPackagesListRow().vm.$emit('packageToDelete', item);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.itemToBeDeleted).toEqual(item);
+ expect(mockModalShow).toHaveBeenCalled();
+ });
+ });
+
+ it('deleteItemConfirmation resets itemToBeDeleted', () => {
+ wrapper.setData({ itemToBeDeleted: 1 });
+ wrapper.vm.deleteItemConfirmation();
+ expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ });
+
+ it('deleteItemConfirmation emit package:delete', () => {
+ const itemToBeDeleted = { id: 2 };
+ wrapper.setData({ itemToBeDeleted });
+ wrapper.vm.deleteItemConfirmation();
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
+ });
+ });
+
+ it('deleteItemCanceled resets itemToBeDeleted', () => {
+ wrapper.setData({ itemToBeDeleted: 1 });
+ wrapper.vm.deleteItemCanceled();
+ expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ });
+ });
+
+ describe('when the list is empty', () => {
+ beforeEach(() => {
+ mountComponent({
+ packages: [],
+ slots: {
+ 'empty-state': EmptySlotStub,
+ },
+ });
+ });
+
+ it('show the empty slot', () => {
+ const emptySlot = findEmptySlot();
+ expect(emptySlot.exists()).toBe(true);
+ });
+ });
+
+ describe('pagination component', () => {
+ let pagination;
+ let modelEvent;
+
+ beforeEach(() => {
+ mountComponent();
+ pagination = findPackageListPagination();
+ // retrieve the event used by v-model, a more sturdy approach than hardcoding it
+ modelEvent = pagination.vm.$options.model.event;
+ });
+
+ it('emits page:changed events when the page changes', () => {
+ pagination.vm.$emit(modelEvent, 2);
+ expect(wrapper.emitted('page:changed')).toEqual([[2]]);
+ });
+ });
+
+ describe('tracking', () => {
+ let eventSpy;
+ let utilSpy;
+ const category = 'foo';
+
+ beforeEach(() => {
+ mountComponent();
+ eventSpy = jest.spyOn(Tracking, 'event');
+ utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
+ wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
+ });
+
+ it('tracking category calls packageTypeToTrackCategory', () => {
+ expect(wrapper.vm.tracking.category).toBe(category);
+ expect(utilSpy).toHaveBeenCalledWith('conan');
+ });
+
+ it('deleteItemConfirmation calls event', () => {
+ wrapper.vm.deleteItemConfirmation();
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.DELETE_PACKAGE,
+ expect.any(Object),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
new file mode 100644
index 00000000000..42bc9fa3a9e
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -0,0 +1,128 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { sortableFields } from '~/packages/list/utils';
+import component from '~/packages_and_registries/package_registry/components/list/package_search.vue';
+import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Package Search', () => {
+ let wrapper;
+ let store;
+
+ const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+ const findUrlSync = () => wrapper.findComponent(UrlSync);
+
+ const createStore = (isGroupPage) => {
+ const state = {
+ config: {
+ isGroupPage,
+ },
+ sorting: {
+ orderBy: 'version',
+ sort: 'desc',
+ },
+ filter: [],
+ };
+ store = new Vuex.Store({
+ state,
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = (isGroupPage = false) => {
+ createStore(isGroupPage);
+
+ wrapper = shallowMount(component, {
+ localVue,
+ store,
+ stubs: {
+ UrlSync,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('has a registry search component', () => {
+ mountComponent();
+
+ expect(findRegistrySearch().exists()).toBe(true);
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: store.state.filter,
+ sorting: store.state.sorting,
+ tokens: expect.arrayContaining([
+ expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ ]),
+ sortableFields: sortableFields(),
+ });
+ });
+
+ it.each`
+ isGroupPage | page
+ ${false} | ${'project'}
+ ${true} | ${'group'}
+ `('in a $page page binds the right props', ({ isGroupPage }) => {
+ mountComponent(isGroupPage);
+
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: store.state.filter,
+ sorting: store.state.sorting,
+ tokens: expect.arrayContaining([
+ expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ ]),
+ sortableFields: sortableFields(isGroupPage),
+ });
+ });
+
+ it('on sorting:changed emits update event and calls vuex setSorting', () => {
+ const payload = { sort: 'foo' };
+
+ mountComponent();
+
+ findRegistrySearch().vm.$emit('sorting:changed', payload);
+
+ expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
+ expect(wrapper.emitted('update')).toEqual([[]]);
+ });
+
+ it('on filter:changed calls vuex setFilter', () => {
+ const payload = ['foo'];
+
+ mountComponent();
+
+ findRegistrySearch().vm.$emit('filter:changed', payload);
+
+ expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
+ });
+
+ it('on filter:submit emits update event', () => {
+ mountComponent();
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(wrapper.emitted('update')).toEqual([[]]);
+ });
+
+ it('has a UrlSync component', () => {
+ mountComponent();
+
+ expect(findUrlSync().exists()).toBe(true);
+ });
+
+ it('on query:changed calls updateQuery from UrlSync', () => {
+ jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
+
+ mountComponent();
+
+ findRegistrySearch().vm.$emit('query:changed');
+
+ expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
new file mode 100644
index 00000000000..3fa96ce1d29
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
@@ -0,0 +1,71 @@
+import { shallowMount } from '@vue/test-utils';
+import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
+import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+
+describe('PackageTitle', () => {
+ let wrapper;
+ let store;
+
+ const findTitleArea = () => wrapper.find(TitleArea);
+ const findMetadataItem = () => wrapper.find(MetadataItem);
+
+ const mountComponent = (propsData = { helpUrl: 'foo' }) => {
+ wrapper = shallowMount(PackageTitle, {
+ store,
+ propsData,
+ stubs: {
+ TitleArea,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('title area', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findTitleArea().exists()).toBe(true);
+ });
+
+ it('has the correct props', () => {
+ mountComponent();
+
+ expect(findTitleArea().props()).toMatchObject({
+ title: LIST_TITLE_TEXT,
+ infoMessages: [{ text: LIST_INTRO_TEXT, link: 'foo' }],
+ });
+ });
+ });
+
+ describe.each`
+ count | exist | text
+ ${null} | ${false} | ${''}
+ ${undefined} | ${false} | ${''}
+ ${0} | ${true} | ${'0 Packages'}
+ ${1} | ${true} | ${'1 Package'}
+ ${2} | ${true} | ${'2 Packages'}
+ `('when count is $count metadata item', ({ count, exist, text }) => {
+ beforeEach(() => {
+ mountComponent({ count, helpUrl: 'foo' });
+ });
+
+ it(`is ${exist} that it exists`, () => {
+ expect(findMetadataItem().exists()).toBe(exist);
+ });
+
+ if (exist) {
+ it('has the correct props', () => {
+ expect(findMetadataItem().props()).toMatchObject({
+ icon: 'package',
+ text,
+ });
+ });
+ }
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
new file mode 100644
index 00000000000..b0cbe34f0b9
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
@@ -0,0 +1,48 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/packages/list/components/tokens/package_type_token.vue';
+import { PACKAGE_TYPES } from '~/packages/list/constants';
+
+describe('packages_filter', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+
+ const mountComponent = ({ attrs, listeners } = {}) => {
+ wrapper = shallowMount(component, {
+ attrs,
+ listeners,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('it binds all of his attrs to filtered search token', () => {
+ mountComponent({ attrs: { foo: 'bar' } });
+
+ expect(findFilteredSearchToken().attributes('foo')).toBe('bar');
+ });
+
+ it('it binds all of his events to filtered search token', () => {
+ const clickListener = jest.fn();
+ mountComponent({ listeners: { click: clickListener } });
+
+ findFilteredSearchToken().vm.$emit('click');
+
+ expect(clickListener).toHaveBeenCalled();
+ });
+
+ it.each(PACKAGE_TYPES.map((p, index) => [p, index]))(
+ 'displays a suggestion for %p',
+ (packageType, index) => {
+ mountComponent();
+ const item = findFilteredSearchSuggestions().at(index);
+ expect(item.text()).toBe(packageType.title);
+ expect(item.props('value')).toBe(packageType.type);
+ },
+ );
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 98ff29ef728..9438a2d2d72 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -133,7 +133,7 @@ export const composerMetadata = () => ({
},
});
-export const pypyMetadata = () => ({
+export const pypiMetadata = () => ({
requiredPython: '1.0.0',
});
@@ -157,7 +157,7 @@ export const packageDetailsQuery = (extendPackage) => ({
metadata: {
...conanMetadata(),
...composerMetadata(),
- ...pypyMetadata(),
+ ...pypiMetadata(),
...mavenMetadata(),
...nugetMetadata(),
},
diff --git a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
index 63c1260560b..f84800d8266 100644
--- a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
+++ b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
@@ -65,7 +65,7 @@ describe('CustomizeHomepageBanner', () => {
await wrapper.vm.$nextTick();
const button = wrapper.find(`[href='${wrapper.vm.preferencesBehaviorPath}']`);
- expect(button.attributes('data-track-event')).toEqual(preferencesTrackingEvent);
+ expect(button.attributes('data-track-action')).toEqual(preferencesTrackingEvent);
expect(button.attributes('data-track-label')).toEqual(provide.trackLabel);
});
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index 4ba9120d196..417567c9f4c 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -11,8 +11,12 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<gl-dropdown-stub
category="primary"
+ clearalltext="Clear all"
headertext=""
hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
+ showhighlighteditemstitle="true"
size="medium"
text="rspec"
variant="default"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
deleted file mode 100644
index 091edc7505c..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
+++ /dev/null
@@ -1,604 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Learn GitLab Design B renders correctly 1`] = `
-<div>
- <div
- class="row"
- >
- <div
- class="gl-mb-7 col-md-8 col-lg-7"
- >
- <h1
- class="gl-font-size-h1"
- >
- Learn GitLab
- </h1>
-
- <p
- class="gl-text-gray-700 gl-mb-0"
- >
- Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.
- </p>
- </div>
- </div>
-
- <div
- class="gl-mb-3"
- >
- <p
- class="gl-text-gray-500 gl-mb-2"
- data-testid="completion-percentage"
- >
- 22% completed
- </p>
-
- <div
- class="progress"
- max="9"
- value="2"
- >
- <div
- aria-valuemax="9"
- aria-valuemin="0"
- aria-valuenow="2"
- class="progress-bar"
- role="progressbar"
- style="width: 22.22222222222222%;"
- />
- </div>
- </div>
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Set up your workspace
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Complete these tasks first so you can enjoy GitLab's features to their fullest:
- </p>
-
- <div
- class="row row-cols-2 row-cols-md-3 row-cols-lg-4"
- >
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <svg
- aria-hidden="true"
- class="gl-text-green-500 gl-icon s16"
- data-testid="completed-icon"
- role="img"
- >
- <use
- href="#check-circle-filled"
- />
- </svg>
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Invite your colleagues"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Invite your colleagues
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- GitLab works best as a team. Invite your colleague to enjoy all features.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Invite your colleagues"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Invite your colleagues
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
-
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <svg
- aria-hidden="true"
- class="gl-text-green-500 gl-icon s16"
- data-testid="completed-icon"
- role="img"
- >
- <use
- href="#check-circle-filled"
- />
- </svg>
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Create or import a repository"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Create or import a repository
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Create or import your first repository into your new project.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Create or import a repository"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Create or import a repository
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
-
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <!---->
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Set-up CI/CD"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Set up CI/CD
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Save time by automating your integration and deployment tasks.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Set-up CI/CD"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Set-up CI/CD
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
-
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <!---->
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Try GitLab Ultimate for free"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Start a free Ultimate trial
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Try all GitLab features for 30 days, no credit card required.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Try GitLab Ultimate for free"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Try GitLab Ultimate for free
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
-
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <span
- class="gl-text-gray-500 gl-font-sm gl-font-style-italic"
- data-testid="trial-only"
- >
- Trial only
- </span>
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Add code owners"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Add code owners
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Prevent unexpected changes to important assets by assigning ownership of files and paths.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Add code owners"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Add code owners
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
-
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <span
- class="gl-text-gray-500 gl-font-sm gl-font-style-italic"
- data-testid="trial-only"
- >
- Trial only
- </span>
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Enable require merge approvals"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Add merge request approval
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Route code reviews to the right reviewers, every time.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Enable require merge approvals"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Enable require merge approvals
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Plan and execute
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Create a workflow for your new workspace, and learn how GitLab features work together:
- </p>
-
- <div
- class="row row-cols-2 row-cols-md-3 row-cols-lg-4"
- >
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <!---->
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Create an issue"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Create an issue
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Create/import issues (tickets) to collaborate on ideas and plan work.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Create an issue"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Create an issue
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
-
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <!---->
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Submit a merge request (MR)"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Submit a merge request
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Review and edit proposed changes to source code.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Submit a merge request (MR)"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Submit a merge request (MR)
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Deploy
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:
- </p>
-
- <div
- class="row row-cols-2 row-cols-lg-4 g-2 g-lg-3"
- >
- <div
- class="col gl-mb-6"
- >
- <div
- class="gl-card gl-pt-0"
- >
- <!---->
-
- <div
- class="gl-card-body"
- >
- <div
- class="gl-text-right gl-h-5"
- >
- <!---->
- </div>
-
- <div
- class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
- >
- <img
- alt="Run a Security scan using CI/CD"
- src="http://example.com/images/illustration.svg"
- />
-
- <h6>
- Run a Security scan using CI/CD
- </h6>
-
- <p
- class="gl-font-sm gl-text-gray-700"
- >
- Scan your code to uncover vulnerabilities before deploying.
- </p>
-
- <a
- class="gl-link"
- data-track-action="click_link"
- data-track-label="Run a Security scan using CI/CD"
- data-track-property="Growth::Activation::Experiment::LearnGitLabB"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Run a Security scan using CI/CD
- </a>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
index 59b42de2485..3aa0e99a858 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Learn GitLab Design A renders correctly 1`] = `
+exports[`Learn GitLab renders correctly 1`] = `
<div>
<div
class="row"
@@ -136,7 +136,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-link"
data-track-action="click_link"
data-track-label="Set up CI/CD"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -157,7 +157,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-link"
data-track-action="click_link"
data-track-label="Start a free Ultimate trial"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -178,7 +178,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-link"
data-track-action="click_link"
data-track-label="Add code owners"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -206,7 +206,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-link"
data-track-action="click_link"
data-track-label="Add merge request approval"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -270,7 +270,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-link"
data-track-action="click_link"
data-track-label="Create an issue"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -291,7 +291,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-link"
data-track-action="click_link"
data-track-label="Submit a merge request"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
@@ -348,7 +348,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-link"
data-track-action="click_link"
data-track-label="Run a Security scan using CI/CD"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
deleted file mode 100644
index 207944bfa1f..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { GlProgressBar } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import LearnGitlabB from '~/pages/projects/learn_gitlab/components/learn_gitlab_b.vue';
-import { testActions } from './mock_data';
-
-describe('Learn GitLab Design B', () => {
- let wrapper;
-
- const createWrapper = () => {
- wrapper = mount(LearnGitlabB, { propsData: { actions: testActions } });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders the progress percentage', () => {
- const text = wrapper.find('[data-testid="completion-percentage"]').text();
-
- expect(text).toBe('22% completed');
- });
-
- it('renders the progress bar with correct values', () => {
- const progressBar = wrapper.findComponent(GlProgressBar);
-
- expect(progressBar.attributes('value')).toBe('2');
- expect(progressBar.attributes('max')).toBe('9');
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
index ac997c1f237..f8099d7e95a 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
@@ -1,13 +1,13 @@
import { GlProgressBar } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
+import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
import { testActions, testSections } from './mock_data';
-describe('Learn GitLab Design A', () => {
+describe('Learn GitLab', () => {
let wrapper;
const createWrapper = () => {
- wrapper = mount(LearnGitlabA, { propsData: { actions: testActions, sections: testSections } });
+ wrapper = mount(LearnGitlab, { propsData: { actions: testActions, sections: testSections } });
};
beforeEach(() => {
diff --git a/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js b/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js
new file mode 100644
index 00000000000..8a7f9229503
--- /dev/null
+++ b/spec/frontend/pages/projects/new/components/new_project_url_select_spec.js
@@ -0,0 +1,122 @@
+import { GlButton, GlDropdown, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import NewProjectUrlSelect from '~/pages/projects/new/components/new_project_url_select.vue';
+import searchQuery from '~/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+
+describe('NewProjectUrlSelect component', () => {
+ let wrapper;
+
+ const data = {
+ currentUser: {
+ groups: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/26',
+ fullPath: 'flightjs',
+ },
+ {
+ id: 'gid://gitlab/Group/28',
+ fullPath: 'h5bp',
+ },
+ ],
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespace/1',
+ fullPath: 'root',
+ },
+ },
+ };
+
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data })]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ const provide = {
+ namespaceFullPath: 'h5bp',
+ namespaceId: '28',
+ rootUrl: 'https://gitlab.com/',
+ trackLabel: 'blank_project',
+ };
+
+ const mountComponent = ({ mountFn = shallowMount } = {}) =>
+ mountFn(NewProjectUrlSelect, { localVue, apolloProvider, provide });
+
+ const findButtonLabel = () => wrapper.findComponent(GlButton);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findHiddenInput = () => wrapper.find('input');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the root url as a label', () => {
+ wrapper = mountComponent();
+
+ expect(findButtonLabel().text()).toBe(provide.rootUrl);
+ expect(findButtonLabel().props('label')).toBe(true);
+ });
+
+ it('renders a dropdown with the initial namespace full path as the text', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().props('text')).toBe(provide.namespaceFullPath);
+ });
+
+ it('renders a dropdown with the initial namespace id in the hidden input', () => {
+ wrapper = mountComponent();
+
+ expect(findHiddenInput().attributes('value')).toBe(provide.namespaceId);
+ });
+
+ it('renders expected dropdown items', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Groups');
+ expect(listItems.at(1).text()).toBe(data.currentUser.groups.nodes[0].fullPath);
+ expect(listItems.at(2).text()).toBe(data.currentUser.groups.nodes[1].fullPath);
+ expect(listItems.at(3).findComponent(GlDropdownSectionHeader).text()).toBe('Users');
+ expect(listItems.at(4).text()).toBe(data.currentUser.namespace.fullPath);
+ });
+
+ it('updates hidden input with selected namespace', async () => {
+ wrapper = mountComponent();
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findHiddenInput().attributes()).toMatchObject({
+ name: 'project[namespace_id]',
+ value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
+ });
+ });
+
+ it('tracks clicking on the dropdown', () => {
+ wrapper = mountComponent();
+
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findDropdown().vm.$emit('show');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_form_input', {
+ label: provide.trackLabel,
+ property: 'project_path',
+ });
+
+ unmockTracking();
+ });
+});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index de0d70a07d7..f3d76ca2c1b 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
@@ -42,11 +42,6 @@ describe('Interval Pattern Input Component', () => {
wrapper = mount(IntervalPatternInput, {
propsData: { ...props },
- provide: {
- glFeatures: {
- ciDailyLimitForPipelineSchedules: true,
- },
- },
data() {
return {
randomHour: data?.hour || mockHour,
diff --git a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
index 6aa725fbd7d..601fcfedbe0 100644
--- a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
+++ b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
@@ -21,7 +21,7 @@ describe('SigninTabsMemoizer', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
});
it('does nothing if no tab was previously selected', () => {
@@ -90,7 +90,7 @@ describe('SigninTabsMemoizer', () => {
});
it('should set .isLocalStorageAvailable', () => {
- expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(AccessorUtilities.canUseLocalStorage).toHaveBeenCalled();
expect(memo.isLocalStorageAvailable).toBe(true);
});
});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
index 39081e07e52..2f934898ef1 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -1,5 +1,6 @@
import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
@@ -48,7 +49,10 @@ describe('Pipeline Editor | Commit section', () => {
let wrapper;
let mockMutate;
- const defaultProps = { ciFileContent: mockCiYml };
+ const defaultProps = {
+ ciFileContent: mockCiYml,
+ commitSha: mockCommitSha,
+ };
const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
mockMutate = jest.fn().mockResolvedValue({
@@ -67,7 +71,6 @@ describe('Pipeline Editor | Commit section', () => {
provide: { ...mockProvide, ...provide },
data() {
return {
- commitSha: mockCommitSha,
currentBranch: mockDefaultBranch,
isNewCiConfigFile: Boolean(options?.isNewCiConfigfile),
};
@@ -97,8 +100,7 @@ describe('Pipeline Editor | Commit section', () => {
await findCommitForm().find('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
}
await findCommitForm().find('[type="submit"]').trigger('click');
- // Simulate the write to local cache that occurs after a commit
- await wrapper.setData({ commitSha: mockCommitNextSha });
+ await waitForPromises();
};
const cancelCommitForm = async () => {
@@ -175,6 +177,10 @@ describe('Pipeline Editor | Commit section', () => {
expect(wrapper.emitted('commit')[0]).toEqual([{ type: COMMIT_SUCCESS }]);
});
+ it('emits an event to refetch the commit sha', () => {
+ expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
+ });
+
it('shows no saving state', () => {
expect(findCommitBtnLoadingIcon().exists()).toBe(false);
});
@@ -188,7 +194,6 @@ describe('Pipeline Editor | Commit section', () => {
update: expect.any(Function),
variables: {
...mockVariables,
- lastCommitId: mockCommitNextSha,
branch: mockDefaultBranch,
},
});
@@ -215,6 +220,10 @@ describe('Pipeline Editor | Commit section', () => {
},
});
});
+
+ it('does not emit an event to refetch the commit sha', () => {
+ expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
+ });
});
describe('when the user commits changes to open a new merge request', () => {
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
index c6c7f593cc5..85222f2ecbb 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
@@ -42,15 +42,12 @@ describe('Pipeline Editor | Text editor component', () => {
defaultBranch: mockDefaultBranch,
glFeatures,
},
+ propsData: {
+ commitSha: mockCommitSha,
+ },
attrs: {
value: mockCiYml,
},
- // Simulate graphQL client query result
- data() {
- return {
- commitSha: mockCommitSha,
- };
- },
listeners: {
[EDITOR_READY_EVENT]: editorReadyListener,
},
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index 85b51d08f88..b5881790b0b 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -247,15 +247,6 @@ describe('Pipeline editor branch switcher', () => {
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
-
- it('emits the updateCommitSha event when selecting a different branch', async () => {
- expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
-
- const branch = findDropdownItems().at(1);
- branch.vm.$emit('click');
-
- expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
- });
});
describe('when searching', () => {
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 94a0a7d14ee..e24de832d6d 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
@@ -4,16 +4,10 @@ import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipelin
describe('Pipeline editor file nav', () => {
let wrapper;
- const mockProvide = {
- glFeatures: {
- pipelineEditorBranchSwitcher: true,
- },
- };
const createComponent = ({ provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorFileNav, {
provide: {
- ...mockProvide,
...provide,
},
});
@@ -34,16 +28,4 @@ describe('Pipeline editor file nav', () => {
expect(findBranchSwitcher().exists()).toBe(true);
});
});
-
- describe('with branch switcher feature flag OFF', () => {
- it('does not render the branch switcher', () => {
- createComponent({
- provide: {
- glFeatures: { pipelineEditorBranchSwitcher: false },
- },
- });
-
- expect(findBranchSwitcher().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
index a95921359cc..753682d438b 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
@@ -27,13 +27,11 @@ describe('Pipeline Status', () => {
wrapper = shallowMount(PipelineStatus, {
localVue,
apolloProvider: mockApollo,
+ propsData: {
+ commitSha: mockCommitSha,
+ },
provide: mockProvide,
stubs: { GlLink, GlSprintf },
- data() {
- return {
- commitSha: mockCommitSha,
- };
- },
});
};
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index 76c68e21180..b019bae886c 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -7,7 +7,6 @@ describe('Pipeline editor empty state', () => {
let wrapper;
const defaultProvide = {
glFeatures: {
- pipelineEditorBranchSwitcher: true,
pipelineEditorEmptyStateAction: false,
},
emptyStateIllustrationPath: 'my/svg/path',
@@ -82,17 +81,5 @@ describe('Pipeline editor empty state', () => {
await findConfirmButton().vm.$emit('click');
expect(wrapper.emitted(expectedEvent)).toHaveLength(1);
});
-
- describe('with branch switcher feature flag OFF', () => {
- it('does not render the file nav', () => {
- createComponent({
- provide: {
- glFeatures: { pipelineEditorBranchSwitcher: false },
- },
- });
-
- expect(findFileNav().exists()).toBe(false);
- });
- });
});
});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index 4d4a8c21d78..f2104f25324 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -156,30 +156,43 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
};
};
-export const mockNewCommitShaResults = {
+export const mockCommitShaResults = {
data: {
project: {
- pipelines: {
- nodes: [
- {
- id: 'gid://gitlab/Ci::Pipeline/1',
- sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca',
- path: `/${mockProjectFullPath}/-/pipelines/488`,
- commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`,
+ repository: {
+ tree: {
+ lastCommit: {
+ sha: mockCommitSha,
},
- {
- id: 'gid://gitlab/Ci::Pipeline/2',
- sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa',
- path: `/${mockProjectFullPath}/-/pipelines/487`,
- commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`,
+ },
+ },
+ },
+ },
+};
+
+export const mockNewCommitShaResults = {
+ data: {
+ project: {
+ repository: {
+ tree: {
+ lastCommit: {
+ sha: 'eeff1122',
},
- {
- id: 'gid://gitlab/Ci::Pipeline/3',
- sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4',
- path: `/${mockProjectFullPath}/-/pipelines/433`,
- commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`,
+ },
+ },
+ },
+ },
+};
+
+export const mockEmptyCommitShaResults = {
+ data: {
+ project: {
+ repository: {
+ tree: {
+ lastCommit: {
+ sha: '',
},
- ],
+ },
},
},
},
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index 0c5c08d7190..393cad0546b 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -26,9 +26,11 @@ import {
mockBlobContentQueryResponseNoCiFile,
mockCiYml,
mockCommitSha,
+ mockCommitShaResults,
mockDefaultBranch,
- mockProjectFullPath,
+ mockEmptyCommitShaResults,
mockNewCommitShaResults,
+ mockProjectFullPath,
} from './mock_data';
const localVue = createLocalVue();
@@ -54,7 +56,6 @@ describe('Pipeline editor app component', () => {
let mockBlobContentData;
let mockCiConfigData;
let mockGetTemplate;
- let mockUpdateCommitSha;
let mockLatestCommitShaQuery;
let mockPipelineQuery;
@@ -71,6 +72,11 @@ describe('Pipeline editor app component', () => {
SourceEditor: MockSourceEditor,
PipelineEditorEmptyState,
},
+ data() {
+ return {
+ commitSha: '',
+ };
+ },
mocks: {
$apollo: {
queries: {
@@ -96,18 +102,7 @@ describe('Pipeline editor app component', () => {
[getPipelineQuery, mockPipelineQuery],
];
- const resolvers = {
- Query: {
- commitSha() {
- return mockCommitSha;
- },
- },
- Mutation: {
- updateCommitSha: mockUpdateCommitSha,
- },
- };
-
- mockApollo = createMockApollo(handlers, resolvers);
+ mockApollo = createMockApollo(handlers);
const options = {
localVue,
@@ -137,7 +132,6 @@ describe('Pipeline editor app component', () => {
mockBlobContentData = jest.fn();
mockCiConfigData = jest.fn();
mockGetTemplate = jest.fn();
- mockUpdateCommitSha = jest.fn();
mockLatestCommitShaQuery = jest.fn();
mockPipelineQuery = jest.fn();
});
@@ -159,11 +153,16 @@ describe('Pipeline editor app component', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
describe('when file exists', () => {
beforeEach(async () => {
await createComponentWithApollo();
+
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
});
it('shows pipeline editor home component', () => {
@@ -181,18 +180,32 @@ describe('Pipeline editor app component', () => {
sha: mockCommitSha,
});
});
+
+ it('does not poll for the commit sha', () => {
+ expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ });
});
describe('when no CI config file exists', () => {
- it('shows an empty state and does not show editor home component', async () => {
+ beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
await createComponentWithApollo();
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+ });
+
+ it('shows an empty state and does not show editor home component', async () => {
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
+ it('does not poll for the commit sha', () => {
+ expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ });
+
describe('because of a fetching error', () => {
it('shows a unkown error message', async () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
@@ -230,6 +243,7 @@ describe('Pipeline editor app component', () => {
describe('when landing on the empty state with feature flag on', () => {
it('user can click on CTA button and see an empty editor', async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
await createComponentWithApollo({
provide: {
@@ -254,9 +268,9 @@ describe('Pipeline editor app component', () => {
const updateSuccessMessage = 'Your changes have been successfully committed.';
describe('and the commit mutation succeeds', () => {
- beforeEach(() => {
+ beforeEach(async () => {
window.scrollTo = jest.fn();
- createComponent();
+ await createComponentWithApollo();
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
});
@@ -268,7 +282,43 @@ describe('Pipeline editor app component', () => {
it('scrolls to the top of the page to bring attention to the confirmation message', () => {
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
});
+
+ it('polls for commit sha while pipeline data is not yet available for current branch', async () => {
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+
+ // simulate a commit to the current branch
+ findEditorHome().vm.$emit('updateCommitSha');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1);
+ });
+
+ it('stops polling for commit sha when pipeline data is available for newly committed branch', async () => {
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
+ .mockImplementation(jest.fn());
+
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ await wrapper.vm.$apollo.queries.commitSha.refetch();
+
+ expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ });
+
+ it('stops polling for commit sha when pipeline data is available for current branch', async () => {
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
+ .mockImplementation(jest.fn());
+
+ mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
+ findEditorHome().vm.$emit('updateCommitSha');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ });
});
+
describe('and the commit mutation fails', () => {
const commitFailedReasons = ['Commit failed'];
@@ -320,6 +370,10 @@ describe('Pipeline editor app component', () => {
});
describe('when refetching content', () => {
+ beforeEach(() => {
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ });
+
it('refetches blob content', async () => {
await createComponentWithApollo();
jest
@@ -352,6 +406,7 @@ describe('Pipeline editor app component', () => {
const originalLocation = window.location.href;
beforeEach(() => {
+ mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
setWindowLocation('?template=Android');
});
@@ -371,45 +426,4 @@ describe('Pipeline editor app component', () => {
expect(findTextEditor().exists()).toBe(true);
});
});
-
- describe('when updating commit sha', () => {
- const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha;
-
- beforeEach(async () => {
- mockUpdateCommitSha.mockResolvedValue(newCommitSha);
- mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
- await createComponentWithApollo();
- });
-
- it('fetches updated commit sha for the new branch', async () => {
- expect(mockLatestCommitShaQuery).not.toHaveBeenCalled();
-
- wrapper
- .findComponent(PipelineEditorHome)
- .vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
- await waitForPromises();
-
- expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({
- projectPath: mockProjectFullPath,
- ref: 'new-branch',
- });
- });
-
- it('updates commit sha with the newly fetched commit sha', async () => {
- expect(mockUpdateCommitSha).not.toHaveBeenCalled();
-
- wrapper
- .findComponent(PipelineEditorHome)
- .vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
- await waitForPromises();
-
- expect(mockUpdateCommitSha).toHaveBeenCalled();
- expect(mockUpdateCommitSha).toHaveBeenCalledWith(
- expect.any(Object),
- { commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha },
- expect.any(Object),
- expect.any(Object),
- );
- });
- });
});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 2a3f4f56f36..9e2bf1bd367 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -45,6 +45,7 @@ describe('Pipeline New Form', () => {
const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
const selectBranch = (branch) => {
@@ -387,7 +388,7 @@ describe('Pipeline New Form', () => {
});
it('does not show the credit card validation required alert', () => {
- expect(wrapper.findComponent(CreditCardValidationRequiredAlert).exists()).toBe(false);
+ expect(findCCAlert().exists()).toBe(false);
});
describe('when the error response is credit card validation required', () => {
@@ -408,7 +409,19 @@ describe('Pipeline New Form', () => {
it('shows credit card validation required alert', () => {
expect(findErrorAlert().exists()).toBe(false);
- expect(wrapper.findComponent(CreditCardValidationRequiredAlert).exists()).toBe(true);
+ expect(findCCAlert().exists()).toBe(true);
+ });
+
+ it('clears error and hides the alert on dismiss', async () => {
+ expect(findCCAlert().exists()).toBe(true);
+ expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]);
+
+ findCCAlert().vm.$emit('dismiss');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findCCAlert().exists()).toBe(false);
+ expect(wrapper.vm.$data.error).toBe(null);
});
});
});
diff --git a/spec/frontend/pipelines/__snapshots__/parsing_utils_spec.js.snap b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
index 60625d301c0..60625d301c0 100644
--- a/spec/frontend/pipelines/__snapshots__/parsing_utils_spec.js.snap
+++ b/spec/frontend/pipelines/__snapshots__/utils_spec.js.snap
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index e0ba6b2e8da..661c8d99477 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -33,8 +33,6 @@ describe('Pipelines filtered search', () => {
};
beforeEach(() => {
- window.gon = { features: { pipelineSourceFilter: true } };
-
mock = new MockAdapter(axios);
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 1fba3823161..4b2b61c8edd 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -1,5 +1,5 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { GRAPHQL, LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
+import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
@@ -54,9 +54,6 @@ describe('graph component', () => {
...data,
};
},
- provide: {
- dataMethod: GRAPHQL,
- },
stubs: {
'links-inner': true,
'linked-pipeline': true,
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 4c7ea5edda9..cbc5d11403e 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -14,7 +14,29 @@ describe('pipeline graph job item', () => {
};
const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
+
+ const delayedJob = {
+ __typename: 'CiJob',
+ name: 'delayed job',
+ scheduledAt: '2015-07-03T10:01:00.000Z',
+ needs: [],
+ status: {
+ __typename: 'DetailedStatus',
+ icon: 'status_scheduled',
+ tooltip: 'delayed manual action (%{remainingTime})',
+ hasDetails: true,
+ detailsPath: '/root/kinder-pipe/-/jobs/5339',
+ group: 'scheduled',
+ action: {
+ __typename: 'StatusAction',
+ icon: 'time-out',
+ title: 'Unschedule',
+ path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule',
+ buttonTitle: 'Unschedule job',
+ },
+ },
+ };
+
const mockJob = {
id: 4256,
name: 'test',
@@ -24,8 +46,8 @@ describe('pipeline graph job item', () => {
label: 'passed',
tooltip: 'passed',
group: 'success',
- details_path: '/root/ci-mock/builds/4256',
- has_details: true,
+ detailsPath: '/root/ci-mock/builds/4256',
+ hasDetails: true,
action: {
icon: 'retry',
title: 'Retry',
@@ -42,8 +64,8 @@ describe('pipeline graph job item', () => {
text: 'passed',
label: 'passed',
group: 'success',
- details_path: '/root/ci-mock/builds/4257',
- has_details: false,
+ detailsPath: '/root/ci-mock/builds/4257',
+ hasDetails: false,
},
};
@@ -58,7 +80,7 @@ describe('pipeline graph job item', () => {
wrapper.vm.$nextTick(() => {
const link = wrapper.find('a');
- expect(link.attributes('href')).toBe(mockJob.status.details_path);
+ expect(link.attributes('href')).toBe(mockJob.status.detailsPath);
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
@@ -145,7 +167,7 @@ describe('pipeline graph job item', () => {
describe('for delayed job', () => {
it('displays remaining time in tooltip', () => {
createWrapper({
- job: delayedJobFixture,
+ job: delayedJob,
});
expect(findJobWithLink().attributes('title')).toBe(
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index c7d95526a0c..af5cd907dd8 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -4,11 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
-import mockData from './linked_pipelines_mock_data';
-
-const mockPipeline = mockData.triggered[0];
-const validTriggeredPipelineId = mockPipeline.project.id;
-const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
+import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => {
let wrapper;
@@ -39,10 +35,10 @@ describe('Linked pipeline', () => {
describe('rendered output', () => {
const props = {
pipeline: mockPipeline,
- projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
+ isLoading: false,
};
beforeEach(() => {
@@ -60,7 +56,7 @@ describe('Linked pipeline', () => {
});
it('should render the pipeline status icon svg', () => {
- expect(wrapper.find('.ci-status-icon-failed svg').exists()).toBe(true);
+ expect(wrapper.find('.ci-status-icon-success svg').exists()).toBe(true);
});
it('should have a ci-status child component', () => {
@@ -73,8 +69,8 @@ describe('Linked pipeline', () => {
it('should correctly compute the tooltip text', () => {
expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name);
+ expect(wrapper.vm.tooltipText).toContain(mockPipeline.status.label);
+ expect(wrapper.vm.tooltipText).toContain(mockPipeline.sourceJob.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
});
@@ -82,11 +78,7 @@ describe('Linked pipeline', () => {
const titleAttr = findLinkedPipeline().attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name);
- expect(titleAttr).toContain(mockPipeline.details.status.label);
- });
-
- it('sets the loading prop to false', () => {
- expect(findButton().props('loading')).toBe(false);
+ expect(titleAttr).toContain(mockPipeline.status.label);
});
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
@@ -96,18 +88,20 @@ describe('Linked pipeline', () => {
describe('parent/child', () => {
const downstreamProps = {
- pipeline: mockPipeline,
- projectId: validTriggeredPipelineId,
+ pipeline: {
+ ...mockPipeline,
+ multiproject: false,
+ },
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
+ isLoading: false,
};
const upstreamProps = {
...downstreamProps,
columnTitle: 'Upstream',
type: UPSTREAM,
- expanded: false,
};
it('parent/child label container should exist', () => {
@@ -122,7 +116,7 @@ describe('Linked pipeline', () => {
it('should have the name of the trigger job on the card when it is a child pipeline', () => {
createWrapper(downstreamProps);
- expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.source_job.name);
+ expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
});
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
@@ -132,12 +126,12 @@ describe('Linked pipeline', () => {
it('downstream pipeline should contain the correct link', () => {
createWrapper(downstreamProps);
- expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
+ expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
});
it('upstream pipeline should contain the correct link', () => {
createWrapper(upstreamProps);
- expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
+ expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path);
});
it.each`
@@ -183,11 +177,11 @@ describe('Linked pipeline', () => {
describe('when isLoading is true', () => {
const props = {
- pipeline: { ...mockPipeline, isLoading: true },
- projectId: invalidTriggeredPipelineId,
+ pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
+ isLoading: true,
};
beforeEach(() => {
@@ -202,10 +196,10 @@ describe('Linked pipeline', () => {
describe('on click/hover', () => {
const props = {
pipeline: mockPipeline,
- projectId: validTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
+ isLoading: false,
};
beforeEach(() => {
@@ -228,7 +222,7 @@ describe('Linked pipeline', () => {
it('should emit downstreamHovered with job name on mouseover', () => {
findLinkedPipeline().trigger('mouseover');
- expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]);
+ expect(wrapper.emitted().downstreamHovered).toStrictEqual([['test_c']]);
});
it('should emit downstreamHovered with empty string on mouseleave', () => {
@@ -238,7 +232,7 @@ describe('Linked pipeline', () => {
it('should emit pipelineExpanded with job name and expanded state on click', () => {
findExpandButton().trigger('click');
- expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['trigger_job', true]]);
+ expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['test_c', true]]);
});
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 24cc6e76098..2f03b846525 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -4,7 +4,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import {
DOWNSTREAM,
- GRAPHQL,
UPSTREAM,
LAYER_VIEW,
STAGE_VIEW,
@@ -52,9 +51,6 @@ describe('Linked Pipelines Column', () => {
...defaultProps,
...props,
},
- provide: {
- dataMethod: GRAPHQL,
- },
});
};
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
index eb05669463b..955b70cbd3b 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
@@ -1,3800 +1,22 @@
export default {
- id: 23211253,
- user: {
- id: 3585,
- name: 'Achilleas Pipinellis',
- username: 'axil',
- state: 'active',
- avatar_url: 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png',
- web_url: 'https://gitlab.com/axil',
- status_tooltip_html:
- '\u003cspan class="user-status-emoji has-tooltip" title="I like pizza" data-html="true" data-placement="top"\u003e\u003cgl-emoji title="slice of pizza" data-name="pizza" data-unicode-version="6.0"\u003e🍕\u003c/gl-emoji\u003e\u003c/span\u003e',
- path: '/axil',
+ __typename: 'Pipeline',
+ id: 195,
+ iid: '5',
+ path: '/root/elemenohpee/-/pipelines/195',
+ status: {
+ __typename: 'DetailedStatus',
+ group: 'success',
+ label: 'passed',
+ icon: 'status_success',
},
- active: false,
- coverage: null,
- source: 'push',
- source_job: {
- name: 'trigger_job',
+ sourceJob: {
+ __typename: 'CiJob',
+ name: 'test_c',
},
- created_at: '2018-06-05T11:31:30.452Z',
- updated_at: '2018-10-31T16:35:31.305Z',
- path: '/gitlab-org/gitlab-runner/pipelines/23211253',
- flags: {
- latest: false,
- stuck: false,
- auto_devops: false,
- merge_request: false,
- yaml_errors: false,
- retryable: false,
- cancelable: false,
- failure_reason: false,
+ project: {
+ __typename: 'Project',
+ name: 'elemenohpee',
+ fullPath: 'root/elemenohpee',
},
- details: {
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/pipelines/23211253',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- duration: 53,
- finished_at: '2018-10-31T16:35:31.299Z',
- stages: [
- {
- name: 'prebuild',
- title: 'prebuild: passed',
- groups: [
- {
- name: 'review-docs-deploy',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'manual play action',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/-/jobs/72469032',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 72469032,
- name: 'review-docs-deploy',
- started: '2018-10-31T16:34:58.778Z',
- archived: false,
- build_path: '/gitlab-org/gitlab-runner/-/jobs/72469032',
- retry_path: '/gitlab-org/gitlab-runner/-/jobs/72469032/retry',
- play_path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play',
- playable: true,
- scheduled: false,
- created_at: '2018-06-05T11:31:30.495Z',
- updated_at: '2018-10-31T16:35:31.251Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'manual play action',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/-/jobs/72469032',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#prebuild',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/gitlab-org/gitlab-runner/pipelines/23211253#prebuild',
- dropdown_path: '/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild',
- },
- {
- name: 'test',
- title: 'test: passed',
- groups: [
- {
- name: 'docs check links',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/-/jobs/72469033',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469033/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 72469033,
- name: 'docs check links',
- started: '2018-06-05T11:31:33.240Z',
- archived: false,
- build_path: '/gitlab-org/gitlab-runner/-/jobs/72469033',
- retry_path: '/gitlab-org/gitlab-runner/-/jobs/72469033/retry',
- playable: false,
- scheduled: false,
- created_at: '2018-06-05T11:31:30.627Z',
- updated_at: '2018-06-05T11:31:54.363Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/-/jobs/72469033',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469033/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#test',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/gitlab-org/gitlab-runner/pipelines/23211253#test',
- dropdown_path: '/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test',
- },
- {
- name: 'cleanup',
- title: 'cleanup: skipped',
- groups: [
- {
- name: 'review-docs-cleanup',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual stop action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/-/jobs/72469034',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'stop',
- title: 'Stop',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play',
- method: 'post',
- button_title: 'Stop this environment',
- },
- },
- jobs: [
- {
- id: 72469034,
- name: 'review-docs-cleanup',
- started: null,
- archived: false,
- build_path: '/gitlab-org/gitlab-runner/-/jobs/72469034',
- play_path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play',
- playable: true,
- scheduled: false,
- created_at: '2018-06-05T11:31:30.760Z',
- updated_at: '2018-06-05T11:31:56.037Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual stop action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/-/jobs/72469034',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'stop',
- title: 'Stop',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play',
- method: 'post',
- button_title: 'Stop this environment',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#cleanup',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-org/gitlab-runner/pipelines/23211253#cleanup',
- dropdown_path: '/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup',
- },
- ],
- artifacts: [],
- manual_actions: [
- {
- name: 'review-docs-cleanup',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'review-docs-deploy',
- path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play',
- playable: true,
- scheduled: false,
- },
- ],
- scheduled_actions: [],
- },
- ref: {
- name: 'docs/add-development-guide-to-readme',
- path: '/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme',
- tag: false,
- branch: true,
- merge_request: false,
- },
- commit: {
- id: '8083eb0a920572214d0dccedd7981f05d535ad46',
- short_id: '8083eb0a',
- title: 'Add link to development guide in readme',
- created_at: '2018-06-05T11:30:48.000Z',
- parent_ids: ['1d7cf79b5a1a2121b9474ac20d61c1b8f621289d'],
- message:
- 'Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n',
- author_name: 'Achilleas Pipinellis',
- author_email: 'axil@gitlab.com',
- authored_date: '2018-06-05T11:30:48.000Z',
- committer_name: 'Achilleas Pipinellis',
- committer_email: 'axil@gitlab.com',
- committed_date: '2018-06-05T11:30:48.000Z',
- author: {
- id: 3585,
- name: 'Achilleas Pipinellis',
- username: 'axil',
- state: 'active',
- avatar_url: 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png',
- web_url: 'https://gitlab.com/axil',
- status_tooltip_html: null,
- path: '/axil',
- },
- author_gravatar_url:
- 'https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon',
- commit_url:
- 'https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46',
- commit_path: '/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46',
- },
- project: { id: 20 },
- triggered_by: {
- id: 12,
- user: {
- id: 376774,
- name: 'Alessio Caiazza',
- username: 'nolith',
- state: 'active',
- avatar_url: 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png',
- web_url: 'https://gitlab.com/nolith',
- status_tooltip_html: null,
- path: '/nolith',
- },
- active: false,
- coverage: null,
- source: 'pipeline',
- source_job: {
- name: 'trigger_job',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- details: {
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- duration: 118,
- finished_at: '2018-10-31T16:41:40.615Z',
- stages: [
- {
- name: 'build-images',
- title: 'build-images: skipped',
- groups: [
- {
- name: 'image:bootstrap',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 11421321982853,
- name: 'image:bootstrap',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.704Z',
- updated_at: '2018-10-31T16:35:24.118Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:builder-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 1149822131854,
- name: 'image:builder-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.728Z',
- updated_at: '2018-10-31T16:35:24.070Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:nginx-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 11498285523424,
- name: 'image:nginx-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.753Z',
- updated_at: '2018-10-31T16:35:24.033Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images',
- },
- {
- name: 'build',
- title: 'build: failed',
- groups: [
- {
- name: 'compile_dev',
- size: 1,
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/528/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 1149846949786,
- name: 'compile_dev',
- started: '2018-10-31T16:39:41.598Z',
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:39:41.138Z',
- updated_at: '2018-10-31T16:41:40.072Z',
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/528/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- recoverable: false,
- },
- ],
- },
- ],
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build',
- },
- {
- name: 'deploy',
- title: 'deploy: skipped',
- groups: [
- {
- name: 'review',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 11498282342357,
- name: 'review',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.805Z',
- updated_at: '2018-10-31T16:41:40.569Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- {
- name: 'review_stop',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 114982858,
- name: 'review_stop',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.840Z',
- updated_at: '2018-10-31T16:41:40.480Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy',
- },
- ],
- artifacts: [],
- manual_actions: [
- {
- name: 'image:bootstrap',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:builder-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:nginx-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'review_stop',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play',
- playable: false,
- scheduled: false,
- },
- ],
- scheduled_actions: [],
- },
- project: {
- id: 20,
- name: 'Test',
- full_path: '/gitlab-com/gitlab-docs',
- full_name: 'GitLab.com / GitLab Docs',
- },
- triggered_by: {
- id: 349932310342451,
- user: {
- id: 376774,
- name: 'Alessio Caiazza',
- username: 'nolith',
- state: 'active',
- avatar_url:
- 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png',
- web_url: 'https://gitlab.com/nolith',
- status_tooltip_html: null,
- path: '/nolith',
- },
- active: false,
- coverage: null,
- source: 'pipeline',
- source_job: {
- name: 'trigger_job',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- details: {
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- duration: 118,
- finished_at: '2018-10-31T16:41:40.615Z',
- stages: [
- {
- name: 'build-images',
- title: 'build-images: skipped',
- groups: [
- {
- name: 'image:bootstrap',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 11421321982853,
- name: 'image:bootstrap',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.704Z',
- updated_at: '2018-10-31T16:35:24.118Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:builder-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 1149822131854,
- name: 'image:builder-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.728Z',
- updated_at: '2018-10-31T16:35:24.070Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:nginx-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 11498285523424,
- name: 'image:nginx-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.753Z',
- updated_at: '2018-10-31T16:35:24.033Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- dropdown_path:
- '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images',
- },
- {
- name: 'build',
- title: 'build: failed',
- groups: [
- {
- name: 'compile_dev',
- size: 1,
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 1149846949786,
- name: 'compile_dev',
- started: '2018-10-31T16:39:41.598Z',
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:39:41.138Z',
- updated_at: '2018-10-31T16:41:40.072Z',
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- recoverable: false,
- },
- ],
- },
- ],
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build',
- },
- {
- name: 'deploy',
- title: 'deploy: skipped',
- groups: [
- {
- name: 'review',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 11498282342357,
- name: 'review',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.805Z',
- updated_at: '2018-10-31T16:41:40.569Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- {
- name: 'review_stop',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 114982858,
- name: 'review_stop',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.840Z',
- updated_at: '2018-10-31T16:41:40.480Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy',
- },
- ],
- artifacts: [],
- manual_actions: [
- {
- name: 'image:bootstrap',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:builder-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:nginx-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'review_stop',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play',
- playable: false,
- scheduled: false,
- },
- ],
- scheduled_actions: [],
- },
- project: {
- id: 20,
- name: 'GitLab Docs',
- full_path: '/gitlab-com/gitlab-docs',
- full_name: 'GitLab.com / GitLab Docs',
- },
- },
- triggered: [],
- },
- triggered: [
- {
- id: 34993051,
- user: {
- id: 376774,
- name: 'Alessio Caiazza',
- username: 'nolith',
- state: 'active',
- avatar_url:
- 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png',
- web_url: 'https://gitlab.com/nolith',
- status_tooltip_html: null,
- path: '/nolith',
- },
- active: false,
- coverage: null,
- source: 'pipeline',
- source_job: {
- name: 'trigger_job',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- details: {
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- duration: 118,
- finished_at: '2018-10-31T16:41:40.615Z',
- stages: [
- {
- name: 'build-images',
- title: 'build-images: skipped',
- groups: [
- {
- name: 'image:bootstrap',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 114982853,
- name: 'image:bootstrap',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.704Z',
- updated_at: '2018-10-31T16:35:24.118Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:builder-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 114982854,
- name: 'image:builder-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.728Z',
- updated_at: '2018-10-31T16:35:24.070Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:nginx-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 114982855,
- name: 'image:nginx-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.753Z',
- updated_at: '2018-10-31T16:35:24.033Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- dropdown_path:
- '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images',
- },
- {
- name: 'build',
- title: 'build: failed',
- groups: [
- {
- name: 'compile_dev',
- size: 1,
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/528/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 114984694,
- name: 'compile_dev',
- started: '2018-10-31T16:39:41.598Z',
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:39:41.138Z',
- updated_at: '2018-10-31T16:41:40.072Z',
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/528/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- recoverable: false,
- },
- ],
- },
- ],
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build',
- },
- {
- name: 'deploy',
- title: 'deploy: skipped',
- groups: [
- {
- name: 'review',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 114982857,
- name: 'review',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.805Z',
- updated_at: '2018-10-31T16:41:40.569Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- {
- name: 'review_stop',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 114982858,
- name: 'review_stop',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.840Z',
- updated_at: '2018-10-31T16:41:40.480Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy',
- },
- ],
- artifacts: [],
- manual_actions: [
- {
- name: 'image:bootstrap',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:builder-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:nginx-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'review_stop',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play',
- playable: false,
- scheduled: false,
- },
- ],
- scheduled_actions: [],
- },
- project: {
- id: 20,
- name: 'GitLab Docs',
- full_path: '/gitlab-com/gitlab-docs',
- full_name: 'GitLab.com / GitLab Docs',
- },
- },
- {
- id: 34993052,
- user: {
- id: 376774,
- name: 'Alessio Caiazza',
- username: 'nolith',
- state: 'active',
- avatar_url:
- 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png',
- web_url: 'https://gitlab.com/nolith',
- status_tooltip_html: null,
- path: '/nolith',
- },
- active: false,
- coverage: null,
- source: 'pipeline',
- source_job: {
- name: 'trigger_job',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- details: {
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- duration: 118,
- finished_at: '2018-10-31T16:41:40.615Z',
- stages: [
- {
- name: 'build-images',
- title: 'build-images: skipped',
- groups: [
- {
- name: 'image:bootstrap',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 114982853,
- name: 'image:bootstrap',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.704Z',
- updated_at: '2018-10-31T16:35:24.118Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:builder-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 114982854,
- name: 'image:builder-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.728Z',
- updated_at: '2018-10-31T16:35:24.070Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- {
- name: 'image:nginx-onbuild',
- size: 1,
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 1224982855,
- name: 'image:nginx-onbuild',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.753Z',
- updated_at: '2018-10-31T16:35:24.033Z',
- status: {
- icon: 'status_manual',
- text: 'manual',
- label: 'manual play action',
- group: 'manual',
- tooltip: 'manual action',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images',
- dropdown_path:
- '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images',
- },
- {
- name: 'build',
- title: 'build: failed',
- groups: [
- {
- name: 'compile_dev',
- size: 1,
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 1123984694,
- name: 'compile_dev',
- started: '2018-10-31T16:39:41.598Z',
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:39:41.138Z',
- updated_at: '2018-10-31T16:41:40.072Z',
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed - (script failure)',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- recoverable: false,
- },
- ],
- },
- ],
- status: {
- icon: 'status_failed',
- text: 'failed',
- label: 'failed',
- group: 'failed',
- tooltip: 'failed',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#build',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build',
- },
- {
- name: 'deploy',
- title: 'deploy: skipped',
- groups: [
- {
- name: 'review',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 1143232982857,
- name: 'review',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.805Z',
- updated_at: '2018-10-31T16:41:40.569Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- {
- name: 'review_stop',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 114921313182858,
- name: 'review_stop',
- started: null,
- archived: false,
- build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- playable: false,
- scheduled: false,
- created_at: '2018-10-31T16:35:23.840Z',
- updated_at: '2018-10-31T16:41:40.480Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858',
- illustration: {
- image:
- 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- illustration: null,
- favicon:
- 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy',
- dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy',
- },
- ],
- artifacts: [],
- manual_actions: [
- {
- name: 'image:bootstrap',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:builder-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'image:nginx-onbuild',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play',
- playable: true,
- scheduled: false,
- },
- {
- name: 'review_stop',
- path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play',
- playable: false,
- scheduled: false,
- },
- ],
- scheduled_actions: [],
- },
- project: {
- id: 20,
- name: 'GitLab Docs',
- full_path: '/gitlab-com/gitlab-docs',
- full_name: 'GitLab.com / GitLab Docs',
- },
- triggered: [
- {
- id: 26,
- user: null,
- active: false,
- coverage: null,
- source: 'push',
- source_job: {
- name: 'trigger_job',
- },
- created_at: '2019-01-06T17:48:37.599Z',
- updated_at: '2019-01-06T17:48:38.371Z',
- path: '/h5bp/html5-boilerplate/pipelines/26',
- flags: {
- latest: true,
- stuck: false,
- auto_devops: false,
- merge_request: false,
- yaml_errors: false,
- retryable: true,
- cancelable: false,
- failure_reason: false,
- },
- details: {
- status: {
- icon: 'status_warning',
- text: 'passed',
- label: 'passed with warnings',
- group: 'success-with-warnings',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/pipelines/26',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- duration: null,
- finished_at: '2019-01-06T17:48:38.370Z',
- stages: [
- {
- name: 'build',
- title: 'build: passed',
- groups: [
- {
- name: 'build:linux',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/526',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/526/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 526,
- name: 'build:linux',
- started: '2019-01-06T08:48:20.236Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/526',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/526/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.806Z',
- updated_at: '2019-01-06T17:48:37.806Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/526',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/526/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'build:osx',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/527',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/527/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 527,
- name: 'build:osx',
- started: '2019-01-06T07:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/527',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/527/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.846Z',
- updated_at: '2019-01-06T17:48:37.846Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/527',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/527/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/pipelines/26#build',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/h5bp/html5-boilerplate/pipelines/26#build',
- dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=build',
- },
- {
- name: 'test',
- title: 'test: passed with warnings',
- groups: [
- {
- name: 'jenkins',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: null,
- group: 'success',
- tooltip: null,
- has_details: false,
- details_path: null,
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- jobs: [
- {
- id: 546,
- name: 'jenkins',
- started: '2019-01-06T11:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/546',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.359Z',
- updated_at: '2019-01-06T17:48:38.359Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: null,
- group: 'success',
- tooltip: null,
- has_details: false,
- details_path: null,
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- },
- ],
- },
- {
- name: 'rspec:linux',
- size: 3,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: false,
- details_path: null,
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- jobs: [
- {
- id: 528,
- name: 'rspec:linux 0 3',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/528',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/528/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.885Z',
- updated_at: '2019-01-06T17:48:37.885Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/528',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/528/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- {
- id: 529,
- name: 'rspec:linux 1 3',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/529',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/529/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.907Z',
- updated_at: '2019-01-06T17:48:37.907Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/529',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/529/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- {
- id: 530,
- name: 'rspec:linux 2 3',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/530',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/530/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.927Z',
- updated_at: '2019-01-06T17:48:37.927Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/530',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/530/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'rspec:osx',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/535',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/535/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 535,
- name: 'rspec:osx',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/535',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/535/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.018Z',
- updated_at: '2019-01-06T17:48:38.018Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/535',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/535/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'rspec:windows',
- size: 3,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: false,
- details_path: null,
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- jobs: [
- {
- id: 531,
- name: 'rspec:windows 0 3',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/531',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/531/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.944Z',
- updated_at: '2019-01-06T17:48:37.944Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/531',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/531/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- {
- id: 532,
- name: 'rspec:windows 1 3',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/532',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/532/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.962Z',
- updated_at: '2019-01-06T17:48:37.962Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/532',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/532/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- {
- id: 534,
- name: 'rspec:windows 2 3',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/534',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/534/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:37.999Z',
- updated_at: '2019-01-06T17:48:37.999Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/534',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/534/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'spinach:linux',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/536',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/536/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 536,
- name: 'spinach:linux',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/536',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/536/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.050Z',
- updated_at: '2019-01-06T17:48:38.050Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/536',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/536/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'spinach:osx',
- size: 1,
- status: {
- icon: 'status_warning',
- text: 'failed',
- label: 'failed (allowed to fail)',
- group: 'failed-with-warnings',
- tooltip: 'failed - (unknown failure) (allowed to fail)',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/537',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/537/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 537,
- name: 'spinach:osx',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/537',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/537/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.069Z',
- updated_at: '2019-01-06T17:48:38.069Z',
- status: {
- icon: 'status_warning',
- text: 'failed',
- label: 'failed (allowed to fail)',
- group: 'failed-with-warnings',
- tooltip: 'failed - (unknown failure) (allowed to fail)',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/537',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/537/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- callout_message: 'There is an unknown failure, please try again',
- recoverable: true,
- },
- ],
- },
- ],
- status: {
- icon: 'status_warning',
- text: 'passed',
- label: 'passed with warnings',
- group: 'success-with-warnings',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/pipelines/26#test',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/h5bp/html5-boilerplate/pipelines/26#test',
- dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=test',
- },
- {
- name: 'security',
- title: 'security: passed',
- groups: [
- {
- name: 'container_scanning',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/541',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/541/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 541,
- name: 'container_scanning',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/541',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/541/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.186Z',
- updated_at: '2019-01-06T17:48:38.186Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/541',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/541/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'dast',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/538',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/538/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 538,
- name: 'dast',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/538',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/538/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.087Z',
- updated_at: '2019-01-06T17:48:38.087Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/538',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/538/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'dependency_scanning',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/540',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/540/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 540,
- name: 'dependency_scanning',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/540',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/540/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.153Z',
- updated_at: '2019-01-06T17:48:38.153Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/540',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/540/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'sast',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/539',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/539/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 539,
- name: 'sast',
- started: '2019-01-06T09:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/539',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/539/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.121Z',
- updated_at: '2019-01-06T17:48:38.121Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/539',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/539/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/pipelines/26#security',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/h5bp/html5-boilerplate/pipelines/26#security',
- dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=security',
- },
- {
- name: 'deploy',
- title: 'deploy: passed',
- groups: [
- {
- name: 'production',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/544',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 544,
- name: 'production',
- started: null,
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/544',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.313Z',
- updated_at: '2019-01-06T17:48:38.313Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/544',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- {
- name: 'staging',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/542',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/542/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- jobs: [
- {
- id: 542,
- name: 'staging',
- started: '2019-01-06T11:48:20.237Z',
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/542',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/542/retry',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.219Z',
- updated_at: '2019-01-06T17:48:38.219Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/542',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job does not have a trace.',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/h5bp/html5-boilerplate/-/jobs/542/retry',
- method: 'post',
- button_title: 'Retry this job',
- },
- },
- },
- ],
- },
- {
- name: 'stop staging',
- size: 1,
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/543',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- jobs: [
- {
- id: 543,
- name: 'stop staging',
- started: null,
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/543',
- playable: false,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.283Z',
- updated_at: '2019-01-06T17:48:38.283Z',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- tooltip: 'skipped',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/543',
- illustration: {
- image:
- '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg',
- size: 'svg-430',
- title: 'This job has been skipped',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png',
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/pipelines/26#deploy',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/h5bp/html5-boilerplate/pipelines/26#deploy',
- dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=deploy',
- },
- {
- name: 'notify',
- title: 'notify: passed',
- groups: [
- {
- name: 'slack',
- size: 1,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'manual play action',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/545',
- illustration: {
- image:
- '/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/h5bp/html5-boilerplate/-/jobs/545/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- jobs: [
- {
- id: 545,
- name: 'slack',
- started: null,
- archived: false,
- build_path: '/h5bp/html5-boilerplate/-/jobs/545',
- retry_path: '/h5bp/html5-boilerplate/-/jobs/545/retry',
- play_path: '/h5bp/html5-boilerplate/-/jobs/545/play',
- playable: true,
- scheduled: false,
- created_at: '2019-01-06T17:48:38.341Z',
- updated_at: '2019-01-06T17:48:38.341Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'manual play action',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/-/jobs/545',
- illustration: {
- image:
- '/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg',
- size: 'svg-394',
- title: 'This job requires a manual action',
- content:
- 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments',
- },
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/h5bp/html5-boilerplate/-/jobs/545/play',
- method: 'post',
- button_title: 'Trigger this manual action',
- },
- },
- },
- ],
- },
- ],
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/h5bp/html5-boilerplate/pipelines/26#notify',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- path: '/h5bp/html5-boilerplate/pipelines/26#notify',
- dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=notify',
- },
- ],
- artifacts: [
- {
- name: 'build:linux',
- expired: null,
- expire_at: null,
- path: '/h5bp/html5-boilerplate/-/jobs/526/artifacts/download',
- browse_path: '/h5bp/html5-boilerplate/-/jobs/526/artifacts/browse',
- },
- {
- name: 'build:osx',
- expired: null,
- expire_at: null,
- path: '/h5bp/html5-boilerplate/-/jobs/527/artifacts/download',
- browse_path: '/h5bp/html5-boilerplate/-/jobs/527/artifacts/browse',
- },
- ],
- manual_actions: [
- {
- name: 'stop staging',
- path: '/h5bp/html5-boilerplate/-/jobs/543/play',
- playable: false,
- scheduled: false,
- },
- {
- name: 'production',
- path: '/h5bp/html5-boilerplate/-/jobs/544/play',
- playable: false,
- scheduled: false,
- },
- {
- name: 'slack',
- path: '/h5bp/html5-boilerplate/-/jobs/545/play',
- playable: true,
- scheduled: false,
- },
- ],
- scheduled_actions: [],
- },
- ref: {
- name: 'main',
- path: '/h5bp/html5-boilerplate/commits/main',
- tag: false,
- branch: true,
- merge_request: false,
- },
- commit: {
- id: 'bad98c453eab56d20057f3929989251d45cd1a8b',
- short_id: 'bad98c45',
- title: 'remove instances of shrink-to-fit=no (#2103)',
- created_at: '2018-12-17T20:52:18.000Z',
- parent_ids: ['49130f6cfe9ff1f749015d735649a2bc6f66cf3a'],
- message:
- 'remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.',
- author_name: "Scott O'Hara",
- author_email: 'scottaohara@users.noreply.github.com',
- authored_date: '2018-12-17T20:52:18.000Z',
- committer_name: 'Rob Larsen',
- committer_email: 'rob@drunkenfist.com',
- committed_date: '2018-12-17T20:52:18.000Z',
- author: null,
- author_gravatar_url:
- 'https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80\u0026d=identicon',
- commit_url:
- 'http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b',
- commit_path: '/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b',
- },
- retry_path: '/h5bp/html5-boilerplate/pipelines/26/retry',
- triggered_by: {
- id: 4,
- user: null,
- active: false,
- coverage: null,
- source: 'push',
- source_job: {
- name: 'trigger_job',
- },
- path: '/gitlab-org/gitlab-test/pipelines/4',
- details: {
- status: {
- icon: 'status_warning',
- text: 'passed',
- label: 'passed with warnings',
- group: 'success-with-warnings',
- tooltip: 'passed',
- has_details: true,
- details_path: '/gitlab-org/gitlab-test/pipelines/4',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
- },
- },
- project: {
- id: 1,
- name: 'Gitlab Test',
- full_path: '/gitlab-org/gitlab-test',
- full_name: 'Gitlab Org / Gitlab Test',
- },
- },
- triggered: [],
- project: {
- id: 20,
- name: 'GitLab Docs',
- full_path: '/gitlab-com/gitlab-docs',
- full_name: 'GitLab.com / GitLab Docs',
- },
- },
- ],
- },
- ],
+ multiproject: true,
};
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index e531e26a858..9e51003da66 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -24,7 +24,7 @@ describe('Pipeline details header', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const defaultProvideOptions = {
- pipelineId: 14,
+ pipelineId: '14',
pipelineIid: 1,
paths: {
pipelinesPath: '/namespace/my-project/-/pipelines',
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index ce33b6011bf..a606595b37d 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlDropdown, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -51,6 +51,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId);
const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId);
const findEmptyMessage = () => wrapper.findByTestId('artifacts-empty-message');
@@ -103,6 +104,15 @@ describe('Pipeline Multi Actions Dropdown', () => {
expect(findEmptyMessage().exists()).toBe(true);
});
+ describe('while loading artifacts', () => {
+ it('should render a loading spinner and no empty message', () => {
+ createComponent({ mockData: { isLoading: true, artifacts: [] } });
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findEmptyMessage().exists()).toBe(false);
+ });
+ });
+
describe('with a failing request', () => {
it('should render an error message', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 76feaaad1ec..aa30062c987 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -105,8 +105,6 @@ describe('Pipelines', () => {
});
beforeEach(() => {
- window.gon = { features: { pipelineSourceFilter: true } };
-
mock = new MockAdapter(axios);
jest.spyOn(window.history, 'pushState');
diff --git a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
index 5d15f0a3c55..684d2d0664a 100644
--- a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
@@ -1,5 +1,6 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants';
import { stubComponent } from 'helpers/stub_component';
import PipelineSourceToken from '~/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue';
@@ -44,7 +45,7 @@ describe('Pipeline Source Token', () => {
describe('shows sources correctly', () => {
it('renders all pipeline sources available', () => {
- expect(findAllFilteredSearchSuggestions()).toHaveLength(wrapper.vm.sources.length);
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(PIPELINE_SOURCES.length);
});
});
});
diff --git a/spec/frontend/pipelines/parsing_utils_spec.js b/spec/frontend/pipelines/utils_spec.js
index 3a270c1c1b5..1c23a7e4fcf 100644
--- a/spec/frontend/pipelines/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/utils_spec.js
@@ -1,6 +1,5 @@
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import {
- createNodeDict,
makeLinksFromNodes,
filterByAncestors,
generateColumnsFromLayersListBare,
@@ -9,6 +8,7 @@ import {
removeOrphanNodes,
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
+import { createNodeDict } from '~/pipelines/utils';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
diff --git a/spec/frontend/pipelines_spec.js b/spec/frontend/pipelines_spec.js
deleted file mode 100644
index add91fbcc23..00000000000
--- a/spec/frontend/pipelines_spec.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Pipelines from '~/pipelines';
-
-describe('Pipelines', () => {
- beforeEach(() => {
- loadFixtures('static/pipeline_graph.html');
- });
-
- it('should be defined', () => {
- expect(Pipelines).toBeDefined();
- });
-
- it('should create a `Pipelines` instance without options', () => {
- expect(() => {
- new Pipelines(); // eslint-disable-line no-new
- }).not.toThrow();
- });
-});
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index 25c509346d1..2751a878e51 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -54,17 +54,20 @@ describe('popovers/components/popovers.vue', () => {
expect(wrapper.findAll(GlPopover)).toHaveLength(1);
});
- it('supports HTML content', async () => {
- const content = 'content with <b>HTML</b>';
- await buildWrapper(
- createPopoverTarget({
- content,
- html: true,
- }),
- );
- const html = wrapper.find(GlPopover).html();
-
- expect(html).toContain(content);
+ describe('supports HTML content', () => {
+ const svgIcon = '<svg><use xlink:href="icons.svg#test"></use></svg>';
+
+ it.each`
+ description | content | render
+ ${'renders html content correctly'} | ${'<b>HTML</b>'} | ${'<b>HTML</b>'}
+ ${'removes any unsafe content'} | ${'<script>alert(XSS)</script>'} | ${''}
+ ${'renders svg icons correctly'} | ${svgIcon} | ${svgIcon}
+ `('$description', async ({ content, render }) => {
+ await buildWrapper(createPopoverTarget({ content, html: true }));
+
+ const html = wrapper.find(GlPopover).html();
+ expect(html).toContain(render);
+ });
});
it.each`
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index b5ee62f2042..6ef49390c47 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -60,7 +60,7 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
- expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
+ expect(chart.props('option')).toBe(wrapper.vm.chartOptions);
});
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 5323c1afbb5..eacf858f22c 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -107,6 +107,29 @@ describe('ServiceDeskSetting', () => {
});
});
+ describe('project suffix', () => {
+ it('input is hidden', () => {
+ wrapper = createComponent({
+ props: { customEmailEnabled: false },
+ });
+
+ const input = wrapper.findByTestId('project-suffix');
+
+ expect(input.exists()).toBe(false);
+ });
+
+ it('input is enabled', () => {
+ wrapper = createComponent({
+ props: { customEmailEnabled: true },
+ });
+
+ const input = wrapper.findByTestId('project-suffix');
+
+ expect(input.exists()).toBe(true);
+ expect(input.attributes('disabled')).toBeUndefined();
+ });
+ });
+
describe('customEmail is the same as incomingEmail', () => {
const email = 'foo@bar.com';
diff --git a/spec/frontend/projects/storage_counter/components/app_spec.js b/spec/frontend/projects/storage_counter/components/app_spec.js
new file mode 100644
index 00000000000..f3da01e0602
--- /dev/null
+++ b/spec/frontend/projects/storage_counter/components/app_spec.js
@@ -0,0 +1,150 @@
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import StorageCounterApp from '~/projects/storage_counter/components/app.vue';
+import { TOTAL_USAGE_DEFAULT_TEXT } from '~/projects/storage_counter/constants';
+import getProjectStorageCount from '~/projects/storage_counter/queries/project_storage.query.graphql';
+import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
+import {
+ mockGetProjectStorageCountGraphQLResponse,
+ mockEmptyResponse,
+ projectData,
+ defaultProvideValues,
+} from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('Storage counter app', () => {
+ let wrapper;
+
+ const createMockApolloProvider = ({ reject = false, mockedValue } = {}) => {
+ let response;
+
+ if (reject) {
+ response = jest.fn().mockRejectedValue(mockedValue || new Error('GraphQL error'));
+ } else {
+ response = jest.fn().mockResolvedValue(mockedValue);
+ }
+
+ const requestHandlers = [[getProjectStorageCount, response]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = ({ provide = {}, mockApollo } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(StorageCounterApp, {
+ localVue,
+ apolloProvider: mockApollo,
+ provide: {
+ ...defaultProvideValues,
+ ...provide,
+ },
+ }),
+ );
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findUsagePercentage = () => wrapper.findByTestId('total-usage');
+ const findUsageQuotasHelpLink = () => wrapper.findByTestId('usage-quotas-help-link');
+ const findUsageGraph = () => wrapper.findComponent(UsageGraph);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with apollo fetching successful', () => {
+ let mockApollo;
+
+ beforeEach(async () => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: mockGetProjectStorageCountGraphQLResponse,
+ });
+ createComponent({ mockApollo });
+ await waitForPromises();
+ });
+
+ it('renders correct total usage', () => {
+ expect(findUsagePercentage().text()).toBe(projectData.storage.totalUsage);
+ });
+
+ it('renders correct usage quotas help link', () => {
+ expect(findUsageQuotasHelpLink().attributes('href')).toBe(
+ defaultProvideValues.helpLinks.usageQuotasHelpPagePath,
+ );
+ });
+ });
+
+ describe('with apollo loading', () => {
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: new Promise(() => {}),
+ });
+ createComponent({ mockApollo });
+ });
+
+ it('should show loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('with apollo returning empty data', () => {
+ let mockApollo;
+
+ beforeEach(async () => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: mockEmptyResponse,
+ });
+ createComponent({ mockApollo });
+ await waitForPromises();
+ });
+
+ it('shows default text for total usage', () => {
+ expect(findUsagePercentage().text()).toBe(TOTAL_USAGE_DEFAULT_TEXT);
+ });
+ });
+
+ describe('with apollo fetching error', () => {
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApolloProvider();
+ createComponent({ mockApollo, reject: true });
+ });
+
+ it('renders gl-alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('rendering <usage-graph />', () => {
+ let mockApollo;
+
+ beforeEach(async () => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: mockGetProjectStorageCountGraphQLResponse,
+ });
+ createComponent({ mockApollo });
+ await waitForPromises();
+ });
+
+ it('renders usage-graph component if project.statistics exists', () => {
+ expect(findUsageGraph().exists()).toBe(true);
+ });
+
+ it('passes project.statistics to usage-graph component', () => {
+ const {
+ __typename,
+ ...statistics
+ } = mockGetProjectStorageCountGraphQLResponse.data.project.statistics;
+ expect(findUsageGraph().props('rootStorageStatistics')).toMatchObject(statistics);
+ });
+ });
+});
diff --git a/spec/frontend/projects/storage_counter/components/storage_table_spec.js b/spec/frontend/projects/storage_counter/components/storage_table_spec.js
new file mode 100644
index 00000000000..14298318fff
--- /dev/null
+++ b/spec/frontend/projects/storage_counter/components/storage_table_spec.js
@@ -0,0 +1,62 @@
+import { GlTable } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import StorageTable from '~/projects/storage_counter/components/storage_table.vue';
+import { projectData, defaultProvideValues } from '../mock_data';
+
+describe('StorageTable', () => {
+ let wrapper;
+
+ const defaultProps = {
+ storageTypes: projectData.storage.storageTypes,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ mount(StorageTable, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ }),
+ );
+ };
+
+ const findTable = () => wrapper.findComponent(GlTable);
+
+ beforeEach(() => {
+ createComponent();
+ });
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with storage types', () => {
+ it.each(projectData.storage.storageTypes)(
+ 'renders table row correctly %o',
+ ({ storageType: { id, name, description } }) => {
+ expect(wrapper.findByTestId(`${id}-name`).text()).toBe(name);
+ expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description);
+ expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe(
+ defaultProvideValues.helpLinks[id.replace(`Size`, `HelpPagePath`)]
+ .replace(`Size`, ``)
+ .replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
+ );
+ },
+ );
+ });
+
+ describe('without storage types', () => {
+ beforeEach(() => {
+ createComponent({ storageTypes: [] });
+ });
+
+ it('should render the table header <th>', () => {
+ expect(findTable().find('th').exists()).toBe(true);
+ });
+
+ it('should not render any table data <td>', () => {
+ expect(findTable().find('td').exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/projects/storage_counter/mock_data.js b/spec/frontend/projects/storage_counter/mock_data.js
new file mode 100644
index 00000000000..b9fa68b3ec7
--- /dev/null
+++ b/spec/frontend/projects/storage_counter/mock_data.js
@@ -0,0 +1,109 @@
+export const mockGetProjectStorageCountGraphQLResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ statistics: {
+ buildArtifactsSize: 400000.0,
+ pipelineArtifactsSize: 25000.0,
+ lfsObjectsSize: 4800000.0,
+ packagesSize: 3800000.0,
+ repositorySize: 3900000.0,
+ snippetsSize: 1200000.0,
+ storageSize: 15300000.0,
+ uploadsSize: 900000.0,
+ wikiSize: 300000.0,
+ __typename: 'ProjectStatistics',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockEmptyResponse = { data: { project: null } };
+
+export const defaultProvideValues = {
+ projectPath: '/project-path',
+ helpLinks: {
+ usageQuotasHelpPagePath: '/usage-quotas',
+ buildArtifactsHelpPagePath: '/build-artifacts',
+ lfsObjectsHelpPagePath: '/lsf-objects',
+ packagesHelpPagePath: '/packages',
+ repositoryHelpPagePath: '/repository',
+ snippetsHelpPagePath: '/snippets',
+ uploadsHelpPagePath: '/uploads',
+ wikiHelpPagePath: '/wiki',
+ },
+};
+
+export const projectData = {
+ storage: {
+ totalUsage: '14.6 MiB',
+ storageTypes: [
+ {
+ storageType: {
+ id: 'buildArtifactsSize',
+ name: 'Artifacts',
+ description: 'Pipeline artifacts and job artifacts, created with CI/CD.',
+ warningMessage:
+ 'There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
+ helpPath: '/build-artifacts',
+ },
+ value: 400000,
+ },
+ {
+ storageType: {
+ id: 'lfsObjectsSize',
+ name: 'LFS Storage',
+ description: 'Audio samples, videos, datasets, and graphics.',
+ helpPath: '/lsf-objects',
+ },
+ value: 4800000,
+ },
+ {
+ storageType: {
+ id: 'packagesSize',
+ name: 'Packages',
+ description: 'Code packages and container images.',
+ helpPath: '/packages',
+ },
+ value: 3800000,
+ },
+ {
+ storageType: {
+ id: 'repositorySize',
+ name: 'Repository',
+ description: 'Git repository, managed by the Gitaly service.',
+ helpPath: '/repository',
+ },
+ value: 3900000,
+ },
+ {
+ storageType: {
+ id: 'snippetsSize',
+ name: 'Snippets',
+ description: 'Shared bits of code and text.',
+ helpPath: '/snippets',
+ },
+ value: 1200000,
+ },
+ {
+ storageType: {
+ id: 'uploadsSize',
+ name: 'Uploads',
+ description: 'File attachments and smaller design graphics.',
+ helpPath: '/uploads',
+ },
+ value: 900000,
+ },
+ {
+ storageType: {
+ id: 'wikiSize',
+ name: 'Wiki',
+ description: 'Wiki content.',
+ helpPath: '/wiki',
+ },
+ value: 300000,
+ },
+ ],
+ },
+};
diff --git a/spec/frontend/projects/storage_counter/utils_spec.js b/spec/frontend/projects/storage_counter/utils_spec.js
new file mode 100644
index 00000000000..57c755266a0
--- /dev/null
+++ b/spec/frontend/projects/storage_counter/utils_spec.js
@@ -0,0 +1,17 @@
+import { parseGetProjectStorageResults } from '~/projects/storage_counter/utils';
+import {
+ mockGetProjectStorageCountGraphQLResponse,
+ projectData,
+ defaultProvideValues,
+} from './mock_data';
+
+describe('parseGetProjectStorageResults', () => {
+ it('parses project statistics correctly', () => {
+ expect(
+ parseGetProjectStorageResults(
+ mockGetProjectStorageCountGraphQLResponse.data,
+ defaultProvideValues.helpLinks,
+ ),
+ ).toMatchObject(projectData);
+ });
+});
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
index 71c22998b08..6576ce70d60 100644
--- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -1,51 +1,91 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
+import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
+import { mockTracking } from 'helpers/tracking_helper';
import TerraformNotification from '~/projects/terraform_notification/components/terraform_notification.vue';
-
-jest.mock('~/lib/utils/common_utils');
+import {
+ EVENT_LABEL,
+ DISMISS_EVENT,
+ CLICK_EVENT,
+} from '~/projects/terraform_notification/constants';
const terraformImagePath = '/path/to/image';
-const bannerDismissedKey = 'terraform_notification_dismissed';
describe('TerraformNotificationBanner', () => {
let wrapper;
+ let trackingSpy;
+ let userCalloutDismissSpy;
const provideData = {
terraformImagePath,
- bannerDismissedKey,
};
const findBanner = () => wrapper.findComponent(GlBanner);
- beforeEach(() => {
+ const createComponent = ({ shouldShowCallout = true } = {}) => {
+ userCalloutDismissSpy = jest.fn();
+
wrapper = shallowMount(TerraformNotification, {
provide: provideData,
- stubs: { GlBanner },
+ stubs: {
+ GlBanner,
+ UserCalloutDismisser: makeMockUserCalloutDismisser({
+ dismiss: userCalloutDismissSpy,
+ shouldShowCallout,
+ }),
+ },
});
+ };
+
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
wrapper.destroy();
- parseBoolean.mockReturnValue(false);
});
- describe('when the dismiss cookie is not set', () => {
+ describe('when user has already dismissed the banner', () => {
+ beforeEach(() => {
+ createComponent({
+ shouldShowCallout: false,
+ });
+ });
+ it('should not render the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+
+ describe("when user hasn't yet dismissed the banner", () => {
it('should render the banner', () => {
expect(findBanner().exists()).toBe(true);
});
});
describe('when close button is clicked', () => {
- beforeEach(async () => {
- await findBanner().vm.$emit('close');
+ beforeEach(() => {
+ wrapper.vm.$refs.calloutDismisser.dismiss = userCalloutDismissSpy;
+ findBanner().vm.$emit('close');
+ });
+ it('should send the dismiss event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, DISMISS_EVENT, {
+ label: EVENT_LABEL,
+ });
});
+ it('should call the dismiss callback', () => {
+ expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
+ });
+ });
- it('should set the cookie with the bannerDismissedKey', () => {
- expect(setCookie).toHaveBeenCalledWith(bannerDismissedKey, true);
+ describe('when docs link is clicked', () => {
+ beforeEach(() => {
+ findBanner().vm.$emit('primary');
});
- it('should remove the banner', () => {
- expect(findBanner().exists()).toBe(false);
+ it('should send button click event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, CLICK_EVENT, {
+ label: EVENT_LABEL,
+ });
});
});
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index d462995328b..8331adcdfc2 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -375,6 +375,30 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('isBinary')).toBe(true);
},
);
+
+ it('passes the correct header props when viewing a non-text file', async () => {
+ fullFactory({
+ mockData: {
+ blobInfo: {
+ ...simpleMockData,
+ simpleViewer: {
+ ...simpleMockData.simpleViewer,
+ fileType: 'image',
+ },
+ },
+ },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+
+ await nextTick();
+
+ expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
+ expect(findBlobHeader().props('isBinary')).toBe(true);
+ expect(findBlobEdit().props('showEditButton')).toBe(false);
+ });
});
describe('BlobButtonGroup', () => {
diff --git a/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
new file mode 100644
index 00000000000..6735dddf51e
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/image_viewer_spec.js
@@ -0,0 +1,25 @@
+import { shallowMount } from '@vue/test-utils';
+import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
+
+describe('Image Viewer', () => {
+ let wrapper;
+
+ const propsData = {
+ url: 'some/image.png',
+ alt: 'image.png',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(ImageViewer, { propsData });
+ };
+
+ const findImage = () => wrapper.find('[data-testid="image"]');
+
+ it('renders a Source Editor component', () => {
+ createComponent();
+
+ expect(findImage().exists()).toBe(true);
+ expect(findImage().attributes('src')).toBe(propsData.url);
+ expect(findImage().attributes('alt')).toBe(propsData.alt);
+ });
+});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 1d1ec58100f..e36287eff29 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import filesQuery from 'shared_queries/repository/files.query.graphql';
+import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from '~/repository/components/tree_content.vue';
@@ -22,6 +22,7 @@ function factory(path, data = () => ({})) {
provide: {
glFeatures: {
increasePageSizeExponentially: true,
+ paginatedTreeGraphqlQuery: true,
},
},
});
@@ -58,7 +59,7 @@ describe('Repository table component', () => {
it('normalizes edge nodes', () => {
factory('/');
- const output = vm.vm.normalizeData('blobs', [{ node: '1' }, { node: '2' }]);
+ const output = vm.vm.normalizeData('blobs', { nodes: ['1', '2'] });
expect(output).toEqual(['1', '2']);
});
@@ -168,7 +169,7 @@ describe('Repository table component', () => {
vm.vm.fetchFiles();
expect($apollo.query).toHaveBeenCalledWith({
- query: filesQuery,
+ query: paginatedTreeQuery,
variables: {
pageSize,
nextPageCursor: '',
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 c1596711be7..3292f635f6b 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -2,6 +2,7 @@ import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -14,16 +15,20 @@ import RunnerPagination from '~/runner/components/runner_pagination.vue';
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
import {
+ ADMIN_FILTERED_SEARCH_NAMESPACE,
CREATED_ASC,
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
PARAM_KEY_STATUS,
+ PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import { captureException } from '~/runner/sentry_utils';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { runnersData, runnersDataPaginated } from '../mock_data';
@@ -47,10 +52,14 @@ describe('AdminRunnersApp', () => {
const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
const findRunnerList = () => wrapper.findComponent(RunnerList);
- const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
+ const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
+ const findRunnerPaginationPrev = () =>
+ findRunnerPagination().findByLabelText('Go to previous page');
+ const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
+ const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
const handlers = [[getRunnersQuery, mockRunnersQuery]];
wrapper = mountFn(AdminRunnersApp, {
@@ -68,7 +77,7 @@ describe('AdminRunnersApp', () => {
setWindowLocation('/admin/runners');
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
- createComponentWithApollo();
+ createComponent();
await waitForPromises();
});
@@ -77,8 +86,16 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
+ it('shows the runner type help', () => {
+ expect(findRunnerTypeHelp().exists()).toBe(true);
+ });
+
+ it('shows the runner setup instructions', () => {
+ expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ });
+
it('shows the runners list', () => {
- expect(runnersData.data.runners.nodes).toMatchObject(findRunnerList().props('runners'));
+ expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
});
it('requests the runners with no filters', () => {
@@ -90,20 +107,38 @@ describe('AdminRunnersApp', () => {
});
});
- it('shows the runner type help', () => {
- expect(findRunnerTypeHelp().exists()).toBe(true);
+ it('sets tokens in the filtered search', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findFilteredSearch().props('tokens')).toEqual([
+ expect.objectContaining({
+ type: PARAM_KEY_STATUS,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
+ type: PARAM_KEY_RUNNER_TYPE,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
+ type: PARAM_KEY_TAG,
+ recentTokenValuesStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
+ }),
+ ]);
});
- it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().exists()).toBe(true);
- expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ it('shows the active runner count', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(
+ `Runners currently online: ${mockActiveRunnersCount}`,
+ );
});
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
- createComponentWithApollo();
+ createComponent();
await waitForPromises();
});
@@ -133,7 +168,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is selected by the user', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
- filters: [{ type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
});
@@ -154,11 +189,19 @@ describe('AdminRunnersApp', () => {
});
});
+ it('when runners have not loaded, shows a loading state', () => {
+ createComponent();
+ expect(findRunnerList().props('loading')).toBe(true);
+ });
+
describe('when no runners are found', () => {
beforeEach(async () => {
- mockRunnersQuery = jest.fn().mockResolvedValue({ data: { runners: { nodes: [] } } });
- createComponentWithApollo();
- await waitForPromises();
+ mockRunnersQuery = jest.fn().mockResolvedValue({
+ data: {
+ runners: { nodes: [] },
+ },
+ });
+ createComponent();
});
it('shows a message for no results', async () => {
@@ -166,17 +209,14 @@ describe('AdminRunnersApp', () => {
});
});
- it('when runners have not loaded, shows a loading state', () => {
- createComponentWithApollo();
- expect(findRunnerList().props('loading')).toBe(true);
- });
-
describe('when runners query fails', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
- createComponentWithApollo();
+ createComponent();
+ });
- await waitForPromises();
+ it('error is shown to the user', async () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
it('error is reported to sentry', async () => {
@@ -185,17 +225,13 @@ describe('AdminRunnersApp', () => {
component: 'AdminRunnersApp',
});
});
-
- it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- });
});
describe('Pagination', () => {
beforeEach(() => {
mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated);
- createComponentWithApollo({ mountFn: mount });
+ createComponent({ mountFn: mount });
});
it('more pages can be selected', () => {
@@ -203,14 +239,11 @@ describe('AdminRunnersApp', () => {
});
it('cannot navigate to the previous page', () => {
- expect(findRunnerPagination().find('[aria-disabled]').text()).toBe('Prev');
+ expect(findRunnerPaginationPrev().attributes('aria-disabled')).toBe('true');
});
it('navigates to the next page', async () => {
- const nextPageBtn = findRunnerPagination().find('a');
- expect(nextPageBtn.text()).toBe('Next');
-
- await nextPageBtn.trigger('click');
+ await findRunnerPaginationNext().trigger('click');
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
sort: CREATED_DESC,
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
index 85cf7ea92df..46948af1f28 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -2,8 +2,16 @@ import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
-import { PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG } from '~/runner/constants';
+import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
+import { typeTokenConfig } from '~/runner/components/search_tokens/type_token_config';
+import {
+ PARAM_KEY_STATUS,
+ PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
+ STATUS_ACTIVE,
+} from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -13,12 +21,12 @@ describe('RunnerList', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem);
- const findActiveRunnersMessage = () => wrapper.findByTestId('active-runners-message');
+ const findActiveRunnersMessage = () => wrapper.findByTestId('runner-count');
const mockDefaultSort = 'CREATED_DESC';
const mockOtherSort = 'CONTACTED_DESC';
const mockFilters = [
- { type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } },
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
];
const mockActiveRunnersCount = 2;
@@ -28,13 +36,16 @@ describe('RunnerList', () => {
shallowMount(RunnerFilteredSearchBar, {
propsData: {
namespace: 'runners',
+ tokens: [],
value: {
filters: [],
sort: mockDefaultSort,
},
- activeRunnersCount: mockActiveRunnersCount,
...props,
},
+ slots: {
+ 'runner-count': `Runners currently online: ${mockActiveRunnersCount}`,
+ },
stubs: {
FilteredSearch,
GlFilteredSearch,
@@ -64,12 +75,6 @@ describe('RunnerList', () => {
);
});
- it('Displays a large active runner count', () => {
- createComponent({ props: { activeRunnersCount: 2000 } });
-
- expect(findActiveRunnersMessage().text()).toBe('Runners currently online: 2,000');
- });
-
it('sets sorting options', () => {
const SORT_OPTIONS_COUNT = 2;
@@ -78,7 +83,13 @@ describe('RunnerList', () => {
expect(findSortOptions().at(1).text()).toBe('Last contact');
});
- it('sets tokens', () => {
+ it('sets tokens to the filtered search', () => {
+ createComponent({
+ props: {
+ tokens: [statusTokenConfig, typeTokenConfig, tagTokenConfig],
+ },
+ });
+
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
type: PARAM_KEY_STATUS,
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 5fff3581e39..344d1e5c150 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -56,7 +56,7 @@ describe('RunnerList', () => {
});
it('Displays a list of runners', () => {
- expect(findRows()).toHaveLength(3);
+ expect(findRows()).toHaveLength(4);
expect(findSkeletonLoader().exists()).toBe(false);
});
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index 15029d7a911..0e0844a785b 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -54,7 +54,7 @@ describe('RunnerUpdateForm', () => {
? ACCESS_LEVEL_REF_PROTECTED
: ACCESS_LEVEL_NOT_PROTECTED,
runUntagged: findRunUntaggedCheckbox().element.checked,
- locked: findLockedCheckbox().element.checked,
+ locked: findLockedCheckbox().element?.checked || false,
ipAddress: findIpInput().element.value,
maximumTimeout: findMaxJobTimeoutInput().element.value || null,
tagList: findTagsInput().element.value.split(',').filter(Boolean),
@@ -153,15 +153,15 @@ describe('RunnerUpdateForm', () => {
});
it.each`
- runnerType | attrDisabled | outcome
- ${INSTANCE_TYPE} | ${'disabled'} | ${'disabled'}
- ${GROUP_TYPE} | ${'disabled'} | ${'disabled'}
- ${PROJECT_TYPE} | ${undefined} | ${'enabled'}
- `(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, attrDisabled }) => {
+ runnerType | exists | outcome
+ ${INSTANCE_TYPE} | ${false} | ${'hidden'}
+ ${GROUP_TYPE} | ${false} | ${'hidden'}
+ ${PROJECT_TYPE} | ${true} | ${'shown'}
+ `(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, exists }) => {
const runner = { ...mockRunner, runnerType };
createComponent({ props: { runner } });
- expect(findLockedCheckbox().attributes('disabled')).toBe(attrDisabled);
+ expect(findLockedCheckbox().exists()).toBe(exists);
});
describe('On submit, runner gets updated', () => {
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 6a0863e92b4..e80da40e3bd 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -1,26 +1,85 @@
-import { shallowMount } from '@vue/test-utils';
+import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { updateHistory } from '~/lib/utils/url_utility';
+
+import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/runner/components/runner_list.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
+
+import {
+ CREATED_ASC,
+ CREATED_DESC,
+ DEFAULT_SORT,
+ INSTANCE_TYPE,
+ PARAM_KEY_STATUS,
+ PARAM_KEY_RUNNER_TYPE,
+ STATUS_ACTIVE,
+ RUNNER_PAGE_SIZE,
+} from '~/runner/constants';
+import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
+import { captureException } from '~/runner/sentry_utils';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
+const mockRunners = groupRunnersData.data.group.runners.nodes;
+const mockGroupRunnersLimitedCount = mockRunners.length;
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
describe('GroupRunnersApp', () => {
let wrapper;
+ let mockGroupRunnersQuery;
const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRunnerList = () => wrapper.findComponent(RunnerList);
+ const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
+ const findRunnerPaginationPrev = () =>
+ findRunnerPagination().findByLabelText('Go to previous page');
+ const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
+ const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
+ const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]];
- const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = mountFn(GroupRunnersApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
propsData: {
registrationToken: mockRegistrationToken,
+ groupFullPath: mockGroupFullPath,
+ groupRunnersLimitedCount: mockGroupRunnersLimitedCount,
+ ...props,
},
});
};
- beforeEach(() => {
+ beforeEach(async () => {
+ setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
+
+ mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
+
createComponent();
+ await waitForPromises();
});
it('shows the runner type help', () => {
@@ -28,7 +87,179 @@ describe('GroupRunnersApp', () => {
});
it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().exists()).toBe(true);
expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
});
+
+ it('shows the runners list', () => {
+ expect(findRunnerList().props('runners')).toEqual(groupRunnersData.data.group.runners.nodes);
+ });
+
+ it('requests the runners with group path and no other filters', () => {
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: undefined,
+ type: undefined,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+
+ it('sets tokens in the filtered search', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findFilteredSearch().props('tokens')).toEqual([
+ expect.objectContaining({
+ type: PARAM_KEY_STATUS,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
+ type: PARAM_KEY_RUNNER_TYPE,
+ options: expect.any(Array),
+ }),
+ ]);
+ });
+
+ describe('shows the active runner count', () => {
+ it('with a regular value', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(
+ `Runners in this group: ${mockGroupRunnersLimitedCount}`,
+ );
+ });
+
+ it('at the limit', () => {
+ createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(`Runners in this group: 1,000`);
+ });
+
+ it('over the limit', () => {
+ createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(`Runners in this group: 1,000+`);
+ });
+ });
+
+ describe('when a filter is preselected', () => {
+ beforeEach(async () => {
+ setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('sets the filters in the search bar', () => {
+ expect(findRunnerFilteredSearchBar().props('value')).toEqual({
+ filters: [
+ { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
+ ],
+ sort: 'CREATED_DESC',
+ pagination: { page: 1 },
+ });
+ });
+
+ it('requests the runners with filter parameters', () => {
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_ACTIVE,
+ type: INSTANCE_TYPE,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+ });
+
+ describe('when a filter is selected by the user', () => {
+ beforeEach(() => {
+ findRunnerFilteredSearchBar().vm.$emit('input', {
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
+ sort: CREATED_ASC,
+ });
+ });
+
+ it('updates the browser url', () => {
+ expect(updateHistory).toHaveBeenLastCalledWith({
+ title: expect.any(String),
+ url: 'http://test.host/groups/group1/-/runners?status[]=ACTIVE&sort=CREATED_ASC',
+ });
+ });
+
+ it('requests the runners with filters', () => {
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_ACTIVE,
+ sort: CREATED_ASC,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+ });
+
+ it('when runners have not loaded, shows a loading state', () => {
+ createComponent();
+ expect(findRunnerList().props('loading')).toBe(true);
+ });
+
+ describe('when no runners are found', () => {
+ beforeEach(async () => {
+ mockGroupRunnersQuery = jest.fn().mockResolvedValue({
+ data: {
+ group: {
+ runners: { nodes: [] },
+ },
+ },
+ });
+ createComponent();
+ });
+
+ it('shows a message for no results', async () => {
+ expect(wrapper.text()).toContain('No runners found');
+ });
+ });
+
+ describe('when runners query fails', () => {
+ beforeEach(() => {
+ mockGroupRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
+ createComponent();
+ });
+
+ it('error is shown to the user', async () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ });
+
+ it('error is reported to sentry', async () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error('Network error: Error!'),
+ component: 'GroupRunnersApp',
+ });
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersDataPaginated);
+
+ createComponent({ mountFn: mount });
+ });
+
+ it('more pages can be selected', () => {
+ expect(findRunnerPagination().text()).toMatchInterpolatedText('Prev Next');
+ });
+
+ it('cannot navigate to the previous page', () => {
+ expect(findRunnerPaginationPrev().attributes('aria-disabled')).toBe('true');
+ });
+
+ it('navigates to the next page', async () => {
+ await findRunnerPaginationNext().trigger('click');
+
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ after: groupRunnersDataPaginated.data.group.runners.pageInfo.endCursor,
+ });
+ });
+ });
});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 8f551feca6e..c90b9a4c426 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -1,6 +1,14 @@
+const runnerFixture = (filename) => getJSONFixture(`graphql/runner/${filename}`);
+
// Fixtures generated by: spec/frontend/fixtures/runner.rb
-export const runnersData = getJSONFixture('graphql/runner/get_runners.query.graphql.json');
-export const runnersDataPaginated = getJSONFixture(
- 'graphql/runner/get_runners.query.graphql.paginated.json',
+
+// Admin queries
+export const runnersData = runnerFixture('get_runners.query.graphql.json');
+export const runnersDataPaginated = runnerFixture('get_runners.query.graphql.paginated.json');
+export const runnerData = runnerFixture('get_runner.query.graphql.json');
+
+// Group queries
+export const groupRunnersData = runnerFixture('get_group_runners.query.graphql.json');
+export const groupRunnersDataPaginated = runnerFixture(
+ 'get_group_runners.query.graphql.paginated.json',
);
-export const runnerData = getJSONFixture('graphql/runner/get_runner.query.graphql.json');
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index 6908bcbd283..9fa3bfc1f9a 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -9,6 +9,6 @@ describe('search/highlight_blob_search_result', () => {
it('highlights lines with search term occurrence', () => {
setHighlightClass(searchKeyword);
- expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
+ expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(4);
});
});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 9f8c83f2873..b50248bb295 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -142,7 +142,13 @@ describe('Global Search Store Actions', () => {
actions.fetchProjects({ commit: mockCommit, state });
expect(Api.groupProjects).not.toHaveBeenCalled();
- expect(Api.projects).toHaveBeenCalled();
+ expect(Api.projects).toHaveBeenCalledWith(
+ state.query.search,
+ {
+ order_by: 'similarity',
+ },
+ expect.any(Function),
+ );
});
});
});
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index cd7f7dc3b5f..bcdad9f89dd 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -14,7 +14,7 @@ const CURRENT_TIME = new Date().getTime();
useLocalStorageSpy();
jest.mock('~/lib/utils/accessor', () => ({
- isLocalStorageAccessSafe: jest.fn().mockReturnValue(true),
+ canUseLocalStorage: jest.fn().mockReturnValue(true),
}));
describe('Global Search Store Utils', () => {
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index fc5eeee9687..455db325066 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -70,8 +70,7 @@ describe('Shortcuts', () => {
const mdShortcuts = $(this).data('md-shortcuts');
// jQuery.map() automatically unwraps arrays, so we
- // have to double wrap the array to counteract this:
- // https://stackoverflow.com/a/4875669/1063392
+ // have to double wrap the array to counteract this
return mdShortcuts ? [mdShortcuts] : undefined;
})
.get();
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 8504684d23a..39f63b2a9f4 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -206,7 +206,7 @@ describe('Sidebar assignees widget', () => {
status: null,
},
],
- id: 1,
+ id: 'gid://gitlab/Issue/1',
},
],
]);
diff --git a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
index 57b9a10b23e..859e63b3df6 100644
--- a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
+++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -45,6 +45,14 @@ describe('Sidebar Participants Widget', () => {
expect(findParticipants().props('loading')).toBe(true);
});
+ it('emits toggleSidebar event when participants child component emits toggleSidebar', async () => {
+ createComponent();
+ findParticipants().vm.$emit('toggleSidebar');
+
+ await nextTick();
+ expect(wrapper.emitted('toggleSidebar')).toEqual([[]]);
+ });
+
describe('when participants are loaded', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/sidebar/sidebar_labels_spec.js b/spec/frontend/sidebar/sidebar_labels_spec.js
index ab08a1e65e2..7455f684380 100644
--- a/spec/frontend/sidebar/sidebar_labels_spec.js
+++ b/spec/frontend/sidebar/sidebar_labels_spec.js
@@ -156,7 +156,7 @@ describe('sidebar labels', () => {
variables: {
input: {
iid: defaultProps.iid,
- labelIds: [toLabelGid(27), toLabelGid(28), toLabelGid(29), toLabelGid(40)],
+ labelIds: [toLabelGid(29), toLabelGid(28), toLabelGid(27), toLabelGid(40)],
operationMode: MutationOperationMode.Replace,
projectPath: defaultProps.projectPath,
},
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index 019ded87093..cb84c142d55 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -63,8 +63,6 @@ describe('Sidebar mediator', () => {
expect(mediator.store.assignees).toEqual(mockData.assignees);
expect(mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
expect(mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
- expect(mediator.store.participants).toEqual(mockData.participants);
- expect(mediator.store.subscribed).toEqual(mockData.subscribed);
expect(mediator.store.timeEstimate).toEqual(mockData.time_estimate);
expect(mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
});
@@ -117,19 +115,4 @@ describe('Sidebar mediator', () => {
urlSpy.mockRestore();
});
});
-
- it('toggle subscription', () => {
- mediator.store.setSubscribedState(false);
- mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
- const spy = jest
- .spyOn(mediator.service, 'toggleSubscription')
- .mockReturnValue(Promise.resolve());
-
- return mediator.toggleSubscription().then(() => {
- expect(spy).toHaveBeenCalled();
- expect(mediator.store.subscribed).toEqual(true);
-
- spy.mockRestore();
- });
- });
});
diff --git a/spec/frontend/sidebar/sidebar_store_spec.js b/spec/frontend/sidebar/sidebar_store_spec.js
index 7b73dc868b7..3930dabfcfa 100644
--- a/spec/frontend/sidebar/sidebar_store_spec.js
+++ b/spec/frontend/sidebar/sidebar_store_spec.js
@@ -16,17 +16,6 @@ const ANOTHER_ASSINEE = {
avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
-const PARTICIPANT = {
- id: 1,
- state: 'active',
- username: 'marcene',
- name: 'Allie Will',
- web_url: 'foo.com',
- avatar_url: 'gravatar.com/avatar/xxx',
-};
-
-const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
-
describe('Sidebar store', () => {
let testContext;
@@ -113,28 +102,6 @@ describe('Sidebar store', () => {
expect(testContext.store.changing).toBe(true);
});
- it('sets participants data', () => {
- expect(testContext.store.participants.length).toEqual(0);
-
- testContext.store.setParticipantsData({
- participants: PARTICIPANT_LIST,
- });
-
- expect(testContext.store.isFetching.participants).toEqual(false);
- expect(testContext.store.participants.length).toEqual(PARTICIPANT_LIST.length);
- });
-
- it('sets subcriptions data', () => {
- expect(testContext.store.subscribed).toEqual(null);
-
- testContext.store.setSubscriptionsData({
- subscribed: true,
- });
-
- expect(testContext.store.isFetching.subscriptions).toEqual(false);
- expect(testContext.store.subscribed).toEqual(true);
- });
-
it('set assigned data', () => {
const users = {
assignees: UsersMockHelper.createNumberRandomUsers(3),
@@ -147,11 +114,11 @@ describe('Sidebar store', () => {
});
it('sets fetching state', () => {
- expect(testContext.store.isFetching.participants).toEqual(true);
+ expect(testContext.store.isFetching.assignees).toEqual(true);
- testContext.store.setFetchingState('participants', false);
+ testContext.store.setFetchingState('assignees', false);
- expect(testContext.store.isFetching.participants).toEqual(false);
+ expect(testContext.store.isFetching.assignees).toEqual(false);
});
it('sets loading state', () => {
diff --git a/spec/frontend/sidebar/track_invite_members_spec.js b/spec/frontend/sidebar/track_invite_members_spec.js
index 6c96e4cfc76..5946e3320c4 100644
--- a/spec/frontend/sidebar/track_invite_members_spec.js
+++ b/spec/frontend/sidebar/track_invite_members_spec.js
@@ -10,7 +10,7 @@ describe('Track user dropdown open', () => {
document.body.innerHTML = `
<div id="dummy-wrapper-element">
<div class="js-sidebar-assignee-dropdown">
- <div class="js-invite-members-track" data-track-event="_track_event_" data-track-label="_track_label_">
+ <div class="js-invite-members-track" data-track-action="_track_event_" data-track-label="_track_label_">
</div>
</div>
</div>
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 22e206bb483..40bc6fe6aa5 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -28,6 +28,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
data-uploads-path=""
>
<markdown-header-stub
+ data-testid="markdownHeader"
linecontent=""
suggestionstartindex="0"
/>
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index a17efdd61a9..21fed51ff10 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -1,10 +1,15 @@
import { setHTMLFixture } from 'helpers/fixtures';
+import { TEST_HOST } from 'helpers/test_constants';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
-import { getExperimentData } from '~/experimentation/utils';
+import { getExperimentData, getAllExperimentContexts } from '~/experimentation/utils';
import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
+import { REFERRER_TTL, URLS_CACHE_STORAGE_KEY } from '~/tracking/constants';
import getStandardContext from '~/tracking/get_standard_context';
-jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
+jest.mock('~/experimentation/utils', () => ({
+ getExperimentData: jest.fn(),
+ getAllExperimentContexts: jest.fn(),
+}));
describe('Tracking', () => {
let standardContext;
@@ -12,9 +17,11 @@ describe('Tracking', () => {
let bindDocumentSpy;
let trackLoadEventsSpy;
let enableFormTracking;
+ let setAnonymousUrlsSpy;
beforeAll(() => {
window.gl = window.gl || {};
+ window.gl.snowplowUrls = {};
window.gl.snowplowStandardContext = {
schema: 'iglu:com.gitlab/gitlab_standard',
data: {
@@ -29,6 +36,7 @@ describe('Tracking', () => {
beforeEach(() => {
getExperimentData.mockReturnValue(undefined);
+ getAllExperimentContexts.mockReturnValue([]);
window.snowplow = window.snowplow || (() => {});
window.snowplowOptions = {
@@ -70,6 +78,7 @@ describe('Tracking', () => {
enableFormTracking = jest
.spyOn(Tracking, 'enableFormTracking')
.mockImplementation(() => null);
+ setAnonymousUrlsSpy = jest.spyOn(Tracking, 'setAnonymousUrls').mockImplementation(() => null);
});
it('should activate features based on what has been enabled', () => {
@@ -100,6 +109,36 @@ describe('Tracking', () => {
initDefaultTrackers();
expect(trackLoadEventsSpy).toHaveBeenCalled();
});
+
+ it('calls the anonymized URLs method', () => {
+ initDefaultTrackers();
+ expect(setAnonymousUrlsSpy).toHaveBeenCalled();
+ });
+
+ describe('when there are experiment contexts', () => {
+ const experimentContexts = [
+ {
+ schema: TRACKING_CONTEXT_SCHEMA,
+ data: { experiment: 'experiment1', variant: 'control' },
+ },
+ {
+ schema: TRACKING_CONTEXT_SCHEMA,
+ data: { experiment: 'experiment_two', variant: 'candidate' },
+ },
+ ];
+
+ beforeEach(() => {
+ getAllExperimentContexts.mockReturnValue(experimentContexts);
+ });
+
+ it('includes those contexts alongside the standard context', () => {
+ initDefaultTrackers();
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [
+ standardContext,
+ ...experimentContexts,
+ ]);
+ });
+ });
});
describe('.event', () => {
@@ -266,6 +305,110 @@ describe('Tracking', () => {
});
});
+ describe('.setAnonymousUrls', () => {
+ afterEach(() => {
+ window.gl.snowplowPseudonymizedPageUrl = '';
+ localStorage.removeItem(URLS_CACHE_STORAGE_KEY);
+ });
+
+ it('does nothing if URLs are not provided', () => {
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalled();
+ expect(localStorage.getItem(URLS_CACHE_STORAGE_KEY)).toBe(null);
+ });
+
+ it('sets the page URL when provided and populates the cache', () => {
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', TEST_HOST);
+ expect(JSON.parse(localStorage.getItem(URLS_CACHE_STORAGE_KEY))[0]).toStrictEqual({
+ url: TEST_HOST,
+ referrer: '',
+ originalUrl: window.location.href,
+ timestamp: Date.now(),
+ });
+ });
+
+ it('appends the hash/fragment to the pseudonymized URL', () => {
+ const hash = 'first-heading';
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+ window.location.hash = hash;
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).toHaveBeenCalledWith('setCustomUrl', `${TEST_HOST}#${hash}`);
+ });
+
+ it('does not set the referrer URL by default', () => {
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', expect.any(String));
+ });
+
+ describe('with referrers cache', () => {
+ const testUrl = '/namespace:1/project:2/-/merge_requests/5';
+ const testOriginalUrl = '/my-namespace/my-project/-/merge_requests/';
+ const setUrlsCache = (data) =>
+ localStorage.setItem(URLS_CACHE_STORAGE_KEY, JSON.stringify(data));
+
+ beforeEach(() => {
+ window.gl.snowplowPseudonymizedPageUrl = TEST_HOST;
+ Object.defineProperty(document, 'referrer', { value: '', configurable: true });
+ });
+
+ it('does nothing if a referrer can not be found', () => {
+ setUrlsCache([
+ {
+ url: testUrl,
+ originalUrl: TEST_HOST,
+ timestamp: Date.now(),
+ },
+ ]);
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', expect.any(String));
+ });
+
+ it('sets referrer URL from the page URL found in cache', () => {
+ Object.defineProperty(document, 'referrer', { value: testOriginalUrl });
+ setUrlsCache([
+ {
+ url: testUrl,
+ originalUrl: testOriginalUrl,
+ timestamp: Date.now(),
+ },
+ ]);
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).toHaveBeenCalledWith('setReferrerUrl', testUrl);
+ });
+
+ it('ignores and removes old entries from the cache', () => {
+ const oldTimestamp = Date.now() - (REFERRER_TTL + 1);
+ Object.defineProperty(document, 'referrer', { value: testOriginalUrl });
+ setUrlsCache([
+ {
+ url: testUrl,
+ originalUrl: testOriginalUrl,
+ timestamp: oldTimestamp,
+ },
+ ]);
+
+ Tracking.setAnonymousUrls();
+
+ expect(snowplowSpy).not.toHaveBeenCalledWith('setReferrerUrl', testUrl);
+ expect(localStorage.getItem(URLS_CACHE_STORAGE_KEY)).not.toContain(oldTimestamp);
+ });
+ });
+ });
+
describe.each`
term
${'event'}
@@ -349,7 +492,7 @@ describe('Tracking', () => {
it('includes experiment data if linked to an experiment', () => {
const mockExperimentData = {
variant: 'candidate',
- experiment: 'repo_integrations_link',
+ experiment: 'example',
key: '2bff73f6bb8cc11156c50a8ba66b9b8b',
};
getExperimentData.mockReturnValue(mockExperimentData);
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
new file mode 100644
index 00000000000..a6c36764c41
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`New ready to merge state component renders permission text if canMerge (false) is false 1`] = `
+<div
+ class="mr-widget-body media"
+>
+ <status-icon-stub
+ status="success"
+ />
+
+ <p
+ class="media-body gl-m-0! gl-font-weight-bold"
+ >
+
+ Ready to merge by members who can write to the target branch.
+
+ </p>
+</div>
+`;
+
+exports[`New ready to merge state component renders permission text if canMerge (true) is false 1`] = `
+<div
+ class="mr-widget-body media"
+>
+ <status-icon-stub
+ status="success"
+ />
+
+ <p
+ class="media-body gl-m-0! gl-font-weight-bold"
+ >
+
+ Ready to merge!
+
+ </p>
+</div>
+`;
diff --git a/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
new file mode 100644
index 00000000000..bdad0bada5f
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/merge_checks_failed_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import MergeChecksFailed from '~/vue_merge_request_widget/components/states/merge_checks_failed.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = shallowMount(MergeChecksFailed, {
+ propsData,
+ });
+}
+
+describe('Merge request widget merge checks failed state component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ mrState | displayText
+ ${{ isPipelineFailed: true }} | ${'pipelineFailed'}
+ ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
+ ${{ hasMergeableDiscussionsState: true }} | ${'unresolvedDiscussions'}
+ `('display $displayText text for $mrState', ({ mrState, displayText }) => {
+ factory({ mr: mrState });
+
+ expect(wrapper.text()).toContain(MergeChecksFailed.i18n[displayText]);
+ });
+
+ describe('unresolved discussions', () => {
+ it('renders jump to button', () => {
+ factory({ mr: { hasMergeableDiscussionsState: true } });
+
+ expect(wrapper.find('[data-testid="jumpToUnresolved"]').exists()).toBe(true);
+ });
+
+ it('renders resolve thread button', () => {
+ factory({
+ mr: {
+ hasMergeableDiscussionsState: true,
+ createIssueToResolveDiscussionsPath: 'https://gitlab.com',
+ },
+ });
+
+ expect(wrapper.find('[data-testid="resolveIssue"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="resolveIssue"]').attributes('href')).toBe(
+ 'https://gitlab.com',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
index c6bfca4516f..e2d79c61b9b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -45,7 +45,7 @@ describe('UnresolvedDiscussions', () => {
expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
- expect(wrapper.element.innerText).toContain('Resolve all threads in new issue');
+ expect(wrapper.element.innerText).toContain('Create issue to resolve all threads');
expect(wrapper.element.querySelector('.js-create-issue').getAttribute('href')).toEqual(
TEST_HOST,
);
@@ -57,7 +57,7 @@ describe('UnresolvedDiscussions', () => {
expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
- expect(wrapper.element.innerText).not.toContain('Resolve all threads in new issue');
+ expect(wrapper.element.innerText).not.toContain('Create issue to resolve all threads');
expect(wrapper.element.querySelector('.js-create-issue')).toEqual(null);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 0609086997b..61e44140efc 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -64,7 +64,7 @@ describe('Wip', () => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
expect(createFlash).toHaveBeenCalledWith({
- message: 'The merge request can now be merged.',
+ message: 'Marked as ready. Merging is now allowed.',
type: 'notice',
});
done();
diff --git a/spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js
new file mode 100644
index 00000000000..5ec9654a4af
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/new_ready_to_merge_spec.js
@@ -0,0 +1,31 @@
+import { shallowMount } from '@vue/test-utils';
+import ReadyToMerge from '~/vue_merge_request_widget/components/states/new_ready_to_merge.vue';
+
+let wrapper;
+
+function factory({ canMerge }) {
+ wrapper = shallowMount(ReadyToMerge, {
+ propsData: {
+ mr: {},
+ },
+ data() {
+ return { canMerge };
+ },
+ });
+}
+
+describe('New ready to merge state component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ canMerge
+ ${true}
+ ${false}
+ `('renders permission text if canMerge ($canMerge) is false', ({ canMerge }) => {
+ factory({ canMerge });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
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 bab928318ce..c7758b0faef 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
@@ -3,9 +3,13 @@
exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-dropdown-stub
category="primary"
+ clearalltext="Clear all"
headertext=""
hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
right="true"
+ showhighlighteditemstitle="true"
size="medium"
text="Clone"
variant="info"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
index db174346729..7f655d67ae8 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Code Block with default props renders correctly 1`] = `
<pre
- class="code-block rounded"
+ class="code-block rounded code"
>
<code
class="d-block"
@@ -14,7 +14,7 @@ exports[`Code Block with default props renders correctly 1`] = `
exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = `
<pre
- class="code-block rounded"
+ class="code-block rounded code"
style="max-height: 200px; overflow-y: auto;"
>
<code
diff --git a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
index f4f9cc288f9..87eaabf4e98 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
@@ -9,7 +9,6 @@ exports[`MemoryGraph Render chart should draw container with chart 1`] = `
data="Nov 12 2019 19:17:33,2.87,Nov 12 2019 19:18:33,2.78,Nov 12 2019 19:19:33,2.78,Nov 12 2019 19:20:33,3.01"
height="25"
tooltiplabel="MB"
- variant="gray900"
/>
</div>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index c4f351eb58d..f2ff12b2acd 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -3,9 +3,13 @@
exports[`SplitButton renders actionItems 1`] = `
<gl-dropdown-stub
category="primary"
+ clearalltext="Clear all"
headertext=""
hideheaderborder="true"
+ highlighteditemstitle="Selected"
+ highlighteditemstitleclass="gl-px-5"
menu-class=""
+ showhighlighteditemstitle="true"
size="medium"
split="true"
text="professor"
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 6a31742141b..d91853e7b79 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -162,8 +162,6 @@ describe('Commit component', () => {
expect(refEl.attributes('href')).toBe(props.commitRef.ref_url);
- expect(refEl.attributes('title')).toBe(props.commitRef.name);
-
expect(findIcon('branch').exists()).toBe(true);
});
});
@@ -195,8 +193,6 @@ describe('Commit component', () => {
expect(refEl.attributes('href')).toBe(props.mergeRequestRef.path);
- expect(refEl.attributes('title')).toBe(props.mergeRequestRef.title);
-
expect(findIcon('git-merge').exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
new file mode 100644
index 00000000000..04f63b4bd45
--- /dev/null
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -0,0 +1,176 @@
+import {
+ GlSprintf,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
+
+jest.mock('fuzzaldrin-plus', () => ({
+ filter: jest.fn().mockReturnValue([]),
+}));
+
+const mockFiles = [
+ {
+ added: 0,
+ href: '#a5cc2925ca8258af241be7e5b0381edf30266302',
+ icon: 'file-modified',
+ iconColor: '',
+ name: '',
+ path: '.gitignore',
+ removed: 3,
+ title: '.gitignore',
+ },
+ {
+ added: 1,
+ href: '#fa288d1472d29beccb489a676f68739ad365fc47',
+ icon: 'file-modified',
+ iconColor: 'danger',
+ name: 'package-lock.json',
+ path: 'lock/file/path',
+ removed: 1,
+ },
+];
+
+describe('Diff Stats Dropdown', () => {
+ let wrapper;
+
+ const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
+ wrapper = shallowMountExtended(DiffStatsDropdown, {
+ propsData: {
+ changed,
+ added,
+ deleted,
+ files,
+ },
+ stubs: {
+ GlSprintf,
+ GlDropdown,
+ },
+ });
+ };
+
+ const findChanged = () => wrapper.findComponent(GlDropdown);
+ const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
+ const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
+ const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
+ const findExpanded = () => wrapper.findByTestId('diff-stats-additions-deletions-collapsed');
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+ describe('file item', () => {
+ beforeEach(() => {
+ createComponent({ files: mockFiles });
+ });
+
+ it('when no file name provided ', () => {
+ expect(findChangedFiles().at(0).text()).toContain(i18n.noFileNameAvailable);
+ });
+
+ it('when all file data is available', () => {
+ const fileData = findChangedFiles().at(1);
+ const fileText = findChangedFiles().at(1).text();
+ expect(fileText).toContain(mockFiles[1].name);
+ expect(fileText).toContain(mockFiles[1].path);
+ expect(fileData.props()).toMatchObject({
+ iconName: mockFiles[1].icon,
+ iconColor: mockFiles[1].iconColor,
+ });
+ });
+
+ it('when no files changed', () => {
+ createComponent({ files: [] });
+ expect(findNoFilesText().text()).toContain(i18n.noFilesFound);
+ });
+ });
+
+ describe.each`
+ changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedExpanded | expectedAddedDeletedCollapsed
+ ${0} | ${0} | ${0} | ${'0 changed files'} | ${'+0 -0'} | ${'with 0 additions and 0 deletions'}
+ ${2} | ${0} | ${2} | ${'2 changed files'} | ${'+0 -2'} | ${'with 0 additions and 2 deletions'}
+ ${2} | ${2} | ${0} | ${'2 changed files'} | ${'+2 -0'} | ${'with 2 additions and 0 deletions'}
+ ${2} | ${1} | ${1} | ${'2 changed files'} | ${'+1 -1'} | ${'with 1 addition and 1 deletion'}
+ ${1} | ${0} | ${1} | ${'1 changed file'} | ${'+0 -1'} | ${'with 0 additions and 1 deletion'}
+ ${1} | ${1} | ${0} | ${'1 changed file'} | ${'+1 -0'} | ${'with 1 addition and 0 deletions'}
+ ${4} | ${2} | ${2} | ${'4 changed files'} | ${'+2 -2'} | ${'with 2 additions and 2 deletions'}
+ `(
+ 'when there are $changed changed file(s), $added added and $deleted deleted file(s)',
+ ({
+ changed,
+ added,
+ deleted,
+ expectedDropdownHeader,
+ expectedAddedDeletedExpanded,
+ expectedAddedDeletedCollapsed,
+ }) => {
+ beforeAll(() => {
+ createComponent({ changed, added, deleted });
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ });
+
+ it(`dropdown header should be '${expectedDropdownHeader}'`, () => {
+ expect(findChanged().props('text')).toBe(expectedDropdownHeader);
+ });
+
+ it(`added and deleted count in expanded section should be '${expectedAddedDeletedExpanded}'`, () => {
+ expect(findExpanded().text()).toBe(expectedAddedDeletedExpanded);
+ });
+
+ it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
+ expect(findCollapsed().text()).toBe(expectedAddedDeletedCollapsed);
+ });
+ },
+ );
+
+ describe('fuzzy file search', () => {
+ beforeEach(() => {
+ createComponent({ files: mockFiles });
+ });
+
+ it('should call `fuzzaldrinPlus.filter` to search for files when the search query is NOT empty', async () => {
+ const searchStr = 'file name';
+ findSearchBox().vm.$emit('input', searchStr);
+ await nextTick();
+ expect(fuzzaldrinPlus.filter).toHaveBeenCalledWith(mockFiles, searchStr, { key: 'name' });
+ });
+
+ it('should NOT call `fuzzaldrinPlus.filter` to search for files when the search query is empty', async () => {
+ const searchStr = '';
+ findSearchBox().vm.$emit('input', searchStr);
+ await nextTick();
+ expect(fuzzaldrinPlus.filter).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('selecting file dropdown item', () => {
+ beforeEach(() => {
+ createComponent({ files: mockFiles });
+ });
+
+ it('updates the URL ', () => {
+ findChangedFiles().at(0).vm.$emit('click');
+ expect(window.location.hash).toBe(mockFiles[0].href);
+ findChangedFiles().at(1).vm.$emit('click');
+ expect(window.location.hash).toBe(mockFiles[1].href);
+ });
+ });
+
+ describe('on dropdown open', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should set the search input focus', () => {
+ wrapper.vm.$refs.search.focusInput = jest.fn();
+ findChanged().vm.$emit('shown');
+
+ expect(wrapper.vm.$refs.search.focusInput).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
index 1b97011bf7f..d85b6e6d115 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -25,7 +25,7 @@ import {
const mockStorageKey = 'recent-tokens';
function setLocalStorageAvailability(isAvailable) {
- jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(isAvailable);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(isAvailable);
}
describe('Filtered Search Utils', () => {
@@ -309,7 +309,7 @@ describe('urlQueryToFilter', () => {
{
[FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
},
- { filteredSearchTermKey: 'search', legacySpacesDecode: false },
+ { filteredSearchTermKey: 'search' },
],
[
'search=my terms&foo=bar&nop=xxx',
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index 529844817d3..bfb593bf82d 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -11,7 +11,10 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
-import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ DEFAULT_MILESTONES,
+ DEFAULT_MILESTONES_GRAPHQL,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data';
@@ -191,5 +194,22 @@ describe('MilestoneToken', () => {
expect(suggestions.at(index).text()).toBe(milestone.text);
});
});
+
+ describe('when getActiveMilestones is called and milestones is empty', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockMilestoneToken, defaultMilestones: DEFAULT_MILESTONES_GRAPHQL },
+ });
+ });
+
+ it('finds the correct value from the activeToken', () => {
+ DEFAULT_MILESTONES_GRAPHQL.forEach(({ value, title }) => {
+ const activeToken = wrapper.vm.getActiveMilestone([], value);
+
+ expect(activeToken.title).toEqual(title);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index b54d120b55b..42f4439df51 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -16,8 +16,6 @@ describe('Header CI Component', () => {
text: 'failed',
details_path: 'path',
},
- itemName: 'job',
- itemId: 123,
time: '2017-05-08T14:57:39.781Z',
user: {
web_url: 'path',
@@ -55,17 +53,13 @@ describe('Header CI Component', () => {
describe('render', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ itemName: 'Pipeline' });
});
it('should render status badge', () => {
expect(findIconBadge().exists()).toBe(true);
});
- it('should render item name and id', () => {
- expect(findHeaderItemText().text()).toBe('job #123');
- });
-
it('should render timeago date', () => {
expect(findTimeAgo().exists()).toBe(true);
});
@@ -83,9 +77,29 @@ describe('Header CI Component', () => {
});
});
+ describe('with item id', () => {
+ beforeEach(() => {
+ createComponent({ itemName: 'Pipeline', itemId: '123' });
+ });
+
+ it('should render item name and id', () => {
+ expect(findHeaderItemText().text()).toBe('Pipeline #123');
+ });
+ });
+
+ describe('without item id', () => {
+ beforeEach(() => {
+ createComponent({ itemName: 'Job build_job' });
+ });
+
+ it('should render item name', () => {
+ expect(findHeaderItemText().text()).toBe('Job build_job');
+ });
+ });
+
describe('slot', () => {
it('should render header action buttons', () => {
- createComponent({}, { slots: { default: 'Test Actions' } });
+ createComponent({ itemName: 'Job build_job' }, { slots: { default: 'Test Actions' } });
expect(findActionButtons().exists()).toBe(true);
expect(findActionButtons().text()).toBe('Test Actions');
@@ -94,7 +108,7 @@ describe('Header CI Component', () => {
describe('shouldRenderTriggeredLabel', () => {
it('should render created keyword when the shouldRenderTriggeredLabel is false', () => {
- createComponent({ shouldRenderTriggeredLabel: false });
+ createComponent({ shouldRenderTriggeredLabel: false, itemName: 'Job build_job' });
expect(wrapper.text()).toContain('created');
expect(wrapper.text()).not.toContain('triggered');
diff --git a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
index 573501233b9..ad8331afcff 100644
--- a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
+++ b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
@@ -1,5 +1,7 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createStore as createMrStore } from '~/mr_notes/stores';
import createIssueStore from '~/notes/stores';
import IssuableHeaderWarnings from '~/vue_shared/components/issuable/issuable_header_warnings.vue';
@@ -12,52 +14,53 @@ localVue.use(Vuex);
describe('IssuableHeaderWarnings', () => {
let wrapper;
- let store;
- const findConfidentialIcon = () => wrapper.find('[data-testid="confidential"]');
- const findLockedIcon = () => wrapper.find('[data-testid="locked"]');
+ const findConfidentialIcon = () => wrapper.findByTestId('confidential');
+ const findLockedIcon = () => wrapper.findByTestId('locked');
+ const findHiddenIcon = () => wrapper.findByTestId('hidden');
const renderTestMessage = (renders) => (renders ? 'renders' : 'does not render');
- const setLock = (locked) => {
- store.getters.getNoteableData.discussion_locked = locked;
- };
-
- const setConfidential = (confidential) => {
- store.getters.getNoteableData.confidential = confidential;
- };
-
- const createComponent = () => {
- wrapper = shallowMount(IssuableHeaderWarnings, { store, localVue });
+ const createComponent = ({ store, provide }) => {
+ wrapper = shallowMountExtended(IssuableHeaderWarnings, {
+ store,
+ localVue,
+ provide,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
- store = null;
});
describe.each`
issuableType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
`(`when issuableType=$issuableType`, ({ issuableType }) => {
- beforeEach(() => {
- store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : createMrStore();
- createComponent();
- });
-
describe.each`
- lockStatus | confidentialStatus
- ${true} | ${true}
- ${true} | ${false}
- ${false} | ${true}
- ${false} | ${false}
+ lockStatus | confidentialStatus | hiddenStatus
+ ${true} | ${true} | ${false}
+ ${true} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ ${false} | ${false} | ${false}
+ ${true} | ${true} | ${true}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${true}
`(
- `when locked=$lockStatus and confidential=$confidentialStatus`,
- ({ lockStatus, confidentialStatus }) => {
+ `when locked=$lockStatus, confidential=$confidentialStatus, and hidden=$hiddenStatus`,
+ ({ lockStatus, confidentialStatus, hiddenStatus }) => {
+ const store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : createMrStore();
+
beforeEach(() => {
- setLock(lockStatus);
- setConfidential(confidentialStatus);
+ store.getters.getNoteableData.confidential = confidentialStatus;
+ store.getters.getNoteableData.discussion_locked = lockStatus;
+
+ createComponent({ store, provide: { hidden: hiddenStatus } });
});
it(`${renderTestMessage(lockStatus)} the locked icon`, () => {
@@ -67,6 +70,19 @@ describe('IssuableHeaderWarnings', () => {
it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => {
expect(findConfidentialIcon().exists()).toBe(confidentialStatus);
});
+
+ it(`${renderTestMessage(confidentialStatus)} the hidden icon`, () => {
+ const hiddenIcon = findHiddenIcon();
+
+ expect(hiddenIcon.exists()).toBe(hiddenStatus);
+
+ if (hiddenStatus) {
+ expect(hiddenIcon.attributes('title')).toBe(
+ 'This issue is hidden because its author has been banned',
+ );
+ expect(getBinding(hiddenIcon.element, 'gl-tooltip')).not.toBeUndefined();
+ }
+ });
},
);
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 442032840e1..76e1a1162ad 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -32,7 +32,7 @@ describe('Markdown field component', () => {
axiosMock.restore();
});
- function createSubject() {
+ function createSubject(lines = []) {
// We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression
// caused by mixing Vanilla JS and Vue.
subject = mount(
@@ -60,6 +60,7 @@ describe('Markdown field component', () => {
markdownPreviewPath,
isSubmitting: false,
textareaValue,
+ lines,
},
},
);
@@ -243,4 +244,14 @@ describe('Markdown field component', () => {
});
});
});
+
+ describe('suggestions', () => {
+ it('escapes new line characters', () => {
+ createSubject([{ rich_text: 'hello world\\n' }]);
+
+ expect(subject.find('[data-testid="markdownHeader"]').props('lineContent')).toBe(
+ 'hello world%br',
+ );
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index fb0009ebb8d..75aa3bc7096 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -135,15 +135,16 @@ describe('title area', () => {
},
});
};
+
it('shows dynamic slots', async () => {
mountComponent();
// we manually add a new slot to simulate dynamic slots being evaluated after the initial mount
wrapper.vm.$slots[DYNAMIC_SLOT] = createDynamicSlot();
+ // updating the slots like we do on line 141 does not cause the updated lifecycle-hook to be triggered
+ wrapper.vm.$forceUpdate();
await wrapper.vm.$nextTick();
- expect(findDynamicSlot().exists()).toBe(false);
- await wrapper.vm.$nextTick();
expect(findDynamicSlot().exists()).toBe(true);
});
@@ -160,10 +161,8 @@ describe('title area', () => {
'metadata-foo': wrapper.vm.$slots['metadata-foo'],
};
- await wrapper.vm.$nextTick();
- expect(findDynamicSlot().exists()).toBe(false);
- expect(findMetadataSlot('metadata-foo').exists()).toBe(true);
-
+ // updating the slots like we do on line 159 does not cause the updated lifecycle-hook to be triggered
+ wrapper.vm.$forceUpdate();
await wrapper.vm.$nextTick();
expect(findSlotOrderElements().at(0).attributes('data-testid')).toBe(DYNAMIC_SLOT);
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
index 69db3ec7132..ad692a38e65 100644
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
@@ -21,6 +21,7 @@ describe('RunnerAwsDeploymentsModal', () => {
wrapper = shallowMount(RunnerAwsDeploymentsModal, {
propsData: {
modalId: 'runner-aws-deployments-modal',
+ imgSrc: '/assets/aws-cloud-formation.png',
},
});
};
diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
index ed085fb66dc..165caea2751 100644
--- a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
+++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
@@ -8,12 +8,25 @@ exports[`Settings Block renders the correct markup 1`] = `
class="settings-header"
>
<h4>
- <div
- data-testid="title-slot"
- />
+ <span
+ aria-controls="settings_content_3"
+ aria-expanded="false"
+ class="gl-cursor-pointer"
+ data-testid="section-title-button"
+ id="settings_label_2"
+ role="button"
+ tabindex="0"
+ >
+ <div
+ data-testid="title-slot"
+ />
+ </span>
</h4>
<gl-button-stub
+ aria-controls="settings_content_3"
+ aria-expanded="false"
+ aria-label="Expand settings section"
buttontextclasses=""
category="primary"
icon=""
@@ -33,7 +46,11 @@ exports[`Settings Block renders the correct markup 1`] = `
</div>
<div
+ aria-labelledby="settings_label_2"
class="settings-content"
+ id="settings_content_3"
+ role="region"
+ tabindex="-1"
>
<div
data-testid="default-slot"
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
index be5a15631eb..528dfd89690 100644
--- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -1,12 +1,12 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/vue_shared/components/settings/settings_block.vue';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
describe('Settings Block', () => {
let wrapper;
const mountComponent = (propsData) => {
- wrapper = shallowMount(component, {
+ wrapper = shallowMount(SettingsBlock, {
propsData,
slots: {
title: '<div data-testid="title-slot"></div>',
@@ -18,13 +18,25 @@ describe('Settings Block', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
const findTitleSlot = () => wrapper.find('[data-testid="title-slot"]');
const findDescriptionSlot = () => wrapper.find('[data-testid="description-slot"]');
- const findExpandButton = () => wrapper.find(GlButton);
+ const findExpandButton = () => wrapper.findComponent(GlButton);
+ const findSectionTitleButton = () => wrapper.find('[data-testid="section-title-button"]');
+
+ const expectExpandedState = ({ expanded = true } = {}) => {
+ const settingsExpandButton = findExpandButton();
+
+ expect(wrapper.classes('expanded')).toBe(expanded);
+ expect(settingsExpandButton.text()).toBe(
+ expanded ? SettingsBlock.i18n.collapseText : SettingsBlock.i18n.expandText,
+ );
+ expect(settingsExpandButton.attributes('aria-label')).toBe(
+ expanded ? SettingsBlock.i18n.collapseAriaLabel : SettingsBlock.i18n.expandAriaLabel,
+ );
+ };
it('renders the correct markup', () => {
mountComponent();
@@ -75,33 +87,41 @@ describe('Settings Block', () => {
it('is collapsed by default', () => {
mountComponent();
- expect(wrapper.classes('expanded')).toBe(false);
+ expectExpandedState({ expanded: false });
});
it('adds expanded class when the expand button is clicked', async () => {
mountComponent();
- expect(wrapper.classes('expanded')).toBe(false);
- expect(findExpandButton().text()).toBe('Expand');
-
await findExpandButton().vm.$emit('click');
- expect(wrapper.classes('expanded')).toBe(true);
- expect(findExpandButton().text()).toBe('Collapse');
+ expectExpandedState({ expanded: true });
});
- it('is expanded when `defaultExpanded` is true no matter what', async () => {
- mountComponent({ defaultExpanded: true });
+ it('adds expanded class when the section title is clicked', async () => {
+ mountComponent();
- expect(wrapper.classes('expanded')).toBe(true);
+ await findSectionTitleButton().trigger('click');
- await findExpandButton().vm.$emit('click');
+ expectExpandedState({ expanded: true });
+ });
- expect(wrapper.classes('expanded')).toBe(true);
+ describe('when `collapsible` is `false`', () => {
+ beforeEach(() => {
+ mountComponent({ collapsible: false });
+ });
- await findExpandButton().vm.$emit('click');
+ it('does not render clickable section title', () => {
+ expect(findSectionTitleButton().exists()).toBe(false);
+ });
+
+ it('contains expanded class', () => {
+ expect(wrapper.classes('expanded')).toBe(true);
+ });
- expect(wrapper.classes('expanded')).toBe(true);
+ it('does not render expand toggle button', () => {
+ expect(findExpandButton().exists()).toBe(false);
+ });
});
});
});
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 a1942e59571..e39e8794fdd 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
@@ -124,7 +124,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('returns false when provided `label` param is not one of the selected labels', () => {
- expect(wrapper.vm.isLabelSelected(mockLabels[2])).toBe(false);
+ expect(wrapper.vm.isLabelSelected(mockLabels[1])).toBe(false);
});
});
@@ -203,7 +203,7 @@ describe('DropdownContentsLabelsView', () => {
it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => {
jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
wrapper.setData({
- currentHighlightItem: 1,
+ currentHighlightItem: 2,
});
wrapper.vm.handleKeyDown({
@@ -213,7 +213,7 @@ describe('DropdownContentsLabelsView', () => {
expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([
{
- ...mockLabels[1],
+ ...mockLabels[2],
set: true,
},
]);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
index c90e63313b2..960ea77cb6e 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
@@ -6,7 +6,7 @@ import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dro
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
-import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data';
+import { mockConfig, mockLabels, mockRegularLabel, mockScopedLabel } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -14,6 +14,9 @@ localVue.use(Vuex);
describe('DropdownValue', () => {
let wrapper;
+ const findAllLabels = () => wrapper.findAllComponents(GlLabel);
+ const findLabel = (index) => findAllLabels().at(index).props('title');
+
const createComponent = (initialState = {}, slots = {}) => {
const store = new Vuex.Store(labelsSelectModule());
@@ -28,7 +31,6 @@ describe('DropdownValue', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('methods', () => {
@@ -82,7 +84,17 @@ describe('DropdownValue', () => {
it('renders labels when `selectedLabels` is not empty', () => {
createComponent();
- expect(wrapper.findAll(GlLabel).length).toBe(2);
+ expect(findAllLabels()).toHaveLength(2);
+ });
+
+ it('orders scoped labels first', () => {
+ createComponent({ selectedLabels: mockLabels });
+
+ expect(findAllLabels()).toHaveLength(mockLabels.length);
+ expect(findLabel(0)).toBe('Foo::Bar');
+ expect(findLabel(1)).toBe('Boog');
+ expect(findLabel(2)).toBe('Bug');
+ expect(findLabel(3)).toBe('Foo Label');
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
index 730afcbecab..1faa3b0af1d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
@@ -15,22 +15,22 @@ export const mockScopedLabel = {
};
export const mockLabels = [
- mockRegularLabel,
- mockScopedLabel,
{
- id: 28,
- title: 'Bug',
+ id: 29,
+ title: 'Boog',
description: 'Label for bugs',
color: '#FF0000',
textColor: '#FFFFFF',
},
{
- id: 29,
- title: 'Boog',
+ id: 28,
+ title: 'Bug',
description: 'Label for bugs',
color: '#FF0000',
textColor: '#FFFFFF',
},
+ mockRegularLabel,
+ mockScopedLabel,
];
export const mockCollapsedLabels = [
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_button_spec.js
deleted file mode 100644
index 0a42d389b67..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_button_spec.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import { GlIcon, GlButton } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-
-import DropdownButton from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue';
-
-import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
-
-import { mockConfig } from './mock_data';
-
-let store;
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const createComponent = (initialState = mockConfig) => {
- store = new Vuex.Store(labelSelectModule());
-
- store.dispatch('setInitialState', initialState);
-
- return shallowMount(DropdownButton, {
- localVue,
- store,
- });
-};
-
-describe('DropdownButton', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findDropdownButton = () => wrapper.find(GlButton);
- const findDropdownText = () => wrapper.find('.dropdown-toggle-text');
- const findDropdownIcon = () => wrapper.find(GlIcon);
-
- describe('methods', () => {
- describe('handleButtonClick', () => {
- it.each`
- variant | expectPropagationStopped
- ${'standalone'} | ${true}
- ${'embedded'} | ${false}
- `(
- 'toggles dropdown content and handles event propagation when `state.variant` is "$variant"',
- ({ variant, expectPropagationStopped }) => {
- const event = { stopPropagation: jest.fn() };
-
- wrapper = createComponent({ ...mockConfig, variant });
-
- findDropdownButton().vm.$emit('click', event);
-
- expect(store.state.showDropdownContents).toBe(true);
- expect(event.stopPropagation).toHaveBeenCalledTimes(expectPropagationStopped ? 1 : 0);
- },
- );
- });
- });
-
- describe('template', () => {
- it('renders component container element', () => {
- expect(wrapper.find(GlButton).element).toBe(wrapper.element);
- });
-
- it('renders default button text element', () => {
- const dropdownTextEl = findDropdownText();
-
- expect(dropdownTextEl.exists()).toBe(true);
- expect(dropdownTextEl.text()).toBe('Label');
- });
-
- it('renders provided button text element', () => {
- store.state.dropdownButtonText = 'Custom label';
- const dropdownTextEl = findDropdownText();
-
- return wrapper.vm.$nextTick().then(() => {
- expect(dropdownTextEl.text()).toBe('Custom label');
- });
- });
-
- it('renders chevron icon element', () => {
- const iconEl = findDropdownIcon();
-
- expect(iconEl.exists()).toBe(true);
- expect(iconEl.props('name')).toBe('chevron-down');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 90bc1980ac3..843298a1406 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -7,7 +7,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
-import { mockSuggestedColors, createLabelSuccessfulResponse } from './mock_data';
+import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
+import {
+ mockSuggestedColors,
+ createLabelSuccessfulResponse,
+ labelsQueryResponse,
+} from './mock_data';
jest.mock('~/flash');
@@ -44,6 +49,14 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: projectLabelsQuery,
+ data: labelsQueryResponse.data,
+ variables: {
+ fullPath: '',
+ searchTerm: '',
+ },
+ });
wrapper = shallowMount(DropdownContentsCreateView, {
localVue,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index 8bd944a3d54..537bbc8e71e 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -45,8 +45,6 @@ describe('DropdownContentsLabelsView', () => {
provide: {
projectPath: 'test',
iid: 1,
- allowLabelCreate: true,
- labelsManagePath: '/gitlab-org/my-project/-/labels',
variant: DropdownVariant.Sidebar,
...injected,
},
@@ -69,10 +67,7 @@ describe('DropdownContentsLabelsView', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findLabelsList = () => wrapper.find('[data-testid="labels-list"]');
- const findDropdownWrapper = () => wrapper.find('[data-testid="dropdown-wrapper"]');
- const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
const findNoResultsMessage = () => wrapper.find('[data-testid="no-results"]');
- const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
describe('when loading labels', () => {
it('renders disabled search input field', async () => {
@@ -109,40 +104,6 @@ describe('DropdownContentsLabelsView', () => {
expect(findLabelsList().exists()).toBe(true);
expect(findLabels()).toHaveLength(2);
});
-
- it('changes highlighted label correctly on pressing down button', async () => {
- expect(findLabels().at(0).attributes('highlight')).toBeUndefined();
-
- await findDropdownWrapper().trigger('keydown.down');
- expect(findLabels().at(0).attributes('highlight')).toBe('true');
-
- await findDropdownWrapper().trigger('keydown.down');
- expect(findLabels().at(1).attributes('highlight')).toBe('true');
- expect(findLabels().at(0).attributes('highlight')).toBeUndefined();
- });
-
- it('changes highlighted label correctly on pressing up button', async () => {
- await findDropdownWrapper().trigger('keydown.down');
- await findDropdownWrapper().trigger('keydown.down');
- expect(findLabels().at(1).attributes('highlight')).toBe('true');
-
- await findDropdownWrapper().trigger('keydown.up');
- expect(findLabels().at(0).attributes('highlight')).toBe('true');
- });
-
- it('changes label selected state when Enter is pressed', async () => {
- expect(findLabels().at(0).attributes('islabelset')).toBeUndefined();
- await findDropdownWrapper().trigger('keydown.down');
- await findDropdownWrapper().trigger('keydown.enter');
-
- expect(findLabels().at(0).attributes('islabelset')).toBe('true');
- });
-
- it('emits `closeDropdown event` when Esc button is pressed', () => {
- findDropdownWrapper().trigger('keydown.esc');
-
- expect(wrapper.emitted('closeDropdown')).toEqual([[selectedLabels]]);
- });
});
it('when search returns 0 results', async () => {
@@ -170,44 +131,4 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
-
- it('does not render footer on standalone dropdown', () => {
- createComponent({ injected: { variant: DropdownVariant.Standalone } });
-
- expect(findDropdownFooter().exists()).toBe(false);
- });
-
- it('renders footer on sidebar dropdown', () => {
- createComponent();
-
- expect(findDropdownFooter().exists()).toBe(true);
- });
-
- it('renders footer on embedded dropdown', () => {
- createComponent({ injected: { variant: DropdownVariant.Embedded } });
-
- expect(findDropdownFooter().exists()).toBe(true);
- });
-
- it('does not render create label button if `allowLabelCreate` is false', () => {
- createComponent({ injected: { allowLabelCreate: false } });
-
- expect(findCreateLabelButton().exists()).toBe(false);
- });
-
- describe('when `allowLabelCreate` is true', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders create label button', () => {
- expect(findCreateLabelButton().exists()).toBe(true);
- });
-
- it('emits `toggleDropdownContentsCreateView` event on create label button click', () => {
- findCreateLabelButton().vm.$emit('click');
-
- expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
index 3c2fd0c5acc..a1b40a891ec 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
@@ -1,77 +1,127 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
+import { GlDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
-
-import { mockConfig, mockLabels } from './mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const createComponent = (initialState = mockConfig, defaultProps = {}) => {
- const store = new Vuex.Store(labelsSelectModule());
-
- store.dispatch('setInitialState', initialState);
-
- return shallowMount(DropdownContents, {
- propsData: {
- ...defaultProps,
- labelsCreateTitle: 'test',
- selectedLabels: mockLabels,
- allowMultiselect: true,
- labelsListTitle: 'Assign labels',
- footerCreateLabelTitle: 'create',
- footerManageLabelTitle: 'manage',
- },
- localVue,
- store,
- });
-};
+import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
+import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
+
+import { mockLabels } from './mock_data';
describe('DropdownContent', () => {
let wrapper;
+ const createComponent = ({ props = {}, injected = {} } = {}) => {
+ wrapper = shallowMount(DropdownContents, {
+ propsData: {
+ labelsCreateTitle: 'test',
+ selectedLabels: mockLabels,
+ allowMultiselect: true,
+ labelsListTitle: 'Assign labels',
+ footerCreateLabelTitle: 'create',
+ footerManageLabelTitle: 'manage',
+ dropdownButtonText: 'Labels',
+ variant: 'sidebar',
+ ...props,
+ },
+ provide: {
+ allowLabelCreate: true,
+ labelsManagePath: 'foo/bar',
+ ...injected,
+ },
+ stubs: {
+ GlDropdown,
+ },
+ });
+ };
+
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
});
- describe('computed', () => {
- describe('dropdownContentsView', () => {
- it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => {
- wrapper.vm.$store.dispatch('toggleDropdownContentsCreateView');
+ const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
+ const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
+ const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
- expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-create-view');
- });
+ describe('Create view', () => {
+ beforeEach(() => {
+ wrapper.vm.toggleDropdownContentsCreateView();
+ });
- it('returns string "dropdown-contents-labels-view" when `showDropdownContentsCreateView` prop is `false`', () => {
- expect(wrapper.vm.dropdownContentsView).toBe('dropdown-contents-labels-view');
- });
+ it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => {
+ expect(wrapper.findComponent(DropdownContentsCreateView).exists()).toBe(true);
+ });
+
+ it('does not render footer', () => {
+ expect(findDropdownFooter().exists()).toBe(false);
+ });
+
+ it('does not render create label button', () => {
+ expect(findCreateLabelButton().exists()).toBe(false);
+ });
+
+ it('renders go back button', () => {
+ expect(findGoBackButton().exists()).toBe(true);
});
});
- describe('template', () => {
- it('renders component container element with class `labels-select-dropdown-contents` and no styles', () => {
- expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents');
- expect(wrapper.attributes('style')).toBeUndefined();
+ describe('Labels view', () => {
+ it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => {
+ expect(wrapper.findComponent(DropdownContentsLabelsView).exists()).toBe(true);
});
- describe('when `renderOnTop` is true', () => {
- it.each`
- variant | expected
- ${DropdownVariant.Sidebar} | ${'bottom: 3rem'}
- ${DropdownVariant.Standalone} | ${'bottom: 2rem'}
- ${DropdownVariant.Embedded} | ${'bottom: 2rem'}
- `('renders upward for $variant variant', ({ variant, expected }) => {
- wrapper = createComponent({ ...mockConfig, variant }, { renderOnTop: true });
+ it('renders footer on sidebar dropdown', () => {
+ expect(findDropdownFooter().exists()).toBe(true);
+ });
+
+ it('does not render footer on standalone dropdown', () => {
+ createComponent({ props: { variant: DropdownVariant.Standalone } });
+
+ expect(findDropdownFooter().exists()).toBe(false);
+ });
- expect(wrapper.attributes('style')).toContain(expected);
+ it('renders footer on embedded dropdown', () => {
+ createComponent({ props: { variant: DropdownVariant.Embedded } });
+
+ expect(findDropdownFooter().exists()).toBe(true);
+ });
+
+ it('does not render go back button', () => {
+ expect(findGoBackButton().exists()).toBe(false);
+ });
+
+ it('does not render create label button if `allowLabelCreate` is false', () => {
+ createComponent({ injected: { allowLabelCreate: false } });
+
+ expect(findCreateLabelButton().exists()).toBe(false);
+ });
+
+ describe('when `allowLabelCreate` is true', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders create label button', () => {
+ expect(findCreateLabelButton().exists()).toBe(true);
});
+
+ it('triggers `toggleDropdownContent` method on create label button click', () => {
+ jest.spyOn(wrapper.vm, 'toggleDropdownContent').mockImplementation(() => {});
+ findCreateLabelButton().trigger('click');
+
+ expect(wrapper.vm.toggleDropdownContent).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `gl-w-full gl-mt-2` and no styles', () => {
+ expect(wrapper.attributes('class')).toContain('gl-w-full gl-mt-2');
+ expect(wrapper.attributes('style')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js
deleted file mode 100644
index d2401a1f725..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_title_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
-
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
-
-import { mockConfig } from './mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const createComponent = (initialState = mockConfig) => {
- const store = new Vuex.Store(labelsSelectModule());
-
- store.dispatch('setInitialState', initialState);
-
- return shallowMount(DropdownTitle, {
- localVue,
- store,
- propsData: {
- labelsSelectInProgress: false,
- },
- });
-};
-
-describe('DropdownTitle', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('template', () => {
- it('renders component container element with string "Labels"', () => {
- expect(wrapper.text()).toContain('Labels');
- });
-
- it('renders edit link', () => {
- const editBtnEl = wrapper.find(GlButton);
-
- expect(editBtnEl.exists()).toBe(true);
- expect(editBtnEl.text()).toBe('Edit');
- });
-
- it('renders loading icon element when `labelsSelectInProgress` prop is true', () => {
- wrapper.setProps({
- labelsSelectInProgress: true,
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
index b3ffee2d020..e7e78cd7a33 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_value_spec.js
@@ -9,8 +9,8 @@ describe('DropdownValue', () => {
let wrapper;
const findAllLabels = () => wrapper.findAllComponents(GlLabel);
- const findRegularLabel = () => findAllLabels().at(0);
- const findScopedLabel = () => findAllLabels().at(1);
+ const findRegularLabel = () => findAllLabels().at(1);
+ const findScopedLabel = () => findAllLabels().at(0);
const findWrapper = () => wrapper.find('[data-testid="value-wrapper"]');
const findEmptyPlaceholder = () => wrapper.find('[data-testid="empty-placeholder"]');
@@ -20,11 +20,13 @@ describe('DropdownValue', () => {
propsData: {
selectedLabels: [mockRegularLabel, mockScopedLabel],
allowLabelRemove: true,
- allowScopedLabels: true,
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
...props,
},
+ provide: {
+ allowScopedLabels: true,
+ },
});
};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js
index 23810339833..6e8841411a2 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js
@@ -1,4 +1,3 @@
-import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
@@ -6,16 +5,10 @@ import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
-const createComponent = ({
- label = mockLabel,
- isLabelSet = mockLabel.set,
- highlight = true,
-} = {}) =>
+const createComponent = ({ label = mockLabel } = {}) =>
shallowMount(LabelItem, {
propsData: {
label,
- isLabelSet,
- highlight,
},
});
@@ -31,45 +24,6 @@ describe('LabelItem', () => {
});
describe('template', () => {
- it('renders gl-link component', () => {
- expect(wrapper.find(GlLink).exists()).toBe(true);
- });
-
- it('renders component root with class `is-focused` when `highlight` prop is true', () => {
- const wrapperTemp = createComponent({
- highlight: true,
- });
-
- expect(wrapperTemp.classes()).toContain('is-focused');
-
- 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('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => {
- const wrapperTemp = createComponent({
- isLabelSet: false,
- });
-
- const placeholderEl = wrapperTemp.find('[data-testid="no-icon"]');
-
- expect(placeholderEl.isVisible()).toBe(true);
-
- wrapperTemp.destroy();
- });
-
it('renders label color element', () => {
const colorEl = wrapper.find('[data-testid="label-color-box"]');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index e17dfd93efc..a18511fa21d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -1,193 +1,74 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-
-import { isInViewport } from '~/lib/utils/common_utils';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import DropdownButton from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue';
+import { shallowMount } from '@vue/test-utils';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
-import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store';
-
import { mockConfig } from './mock_data';
-jest.mock('~/lib/utils/common_utils', () => ({
- isInViewport: jest.fn().mockReturnValue(true),
-}));
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
describe('LabelsSelectRoot', () => {
let wrapper;
- let store;
const createComponent = (config = mockConfig, slots = {}) => {
wrapper = shallowMount(LabelsSelectRoot, {
- localVue,
slots,
- store,
propsData: config,
stubs: {
- 'dropdown-contents': DropdownContents,
+ DropdownContents,
+ SidebarEditableItem,
},
provide: {
iid: '1',
projectPath: 'test',
+ canUpdate: true,
+ allowLabelEdit: true,
},
});
};
- beforeEach(() => {
- store = new Vuex.Store(labelsSelectModule());
- });
-
afterEach(() => {
wrapper.destroy();
});
- describe('methods', () => {
- 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();
- });
- });
-
- describe('handleCollapsedValueClick', () => {
- it('emits `toggleCollapse` event on component', () => {
- createComponent();
- wrapper.vm.handleCollapsedValueClick();
-
- expect(wrapper.emitted().toggleCollapse).toBeTruthy();
- });
- });
+ it('renders component with classes `labels-select-wrapper position-relative`', () => {
+ createComponent();
+ expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'position-relative']);
});
- describe('template', () => {
- it('renders component with classes `labels-select-wrapper position-relative`', () => {
- createComponent();
- expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
- });
-
- it.each`
- variant | cssClass
- ${'standalone'} | ${'is-standalone'}
- ${'embedded'} | ${'is-embedded'}
- `(
- 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
- ({ variant, cssClass }) => {
- createComponent({
- ...mockConfig,
- variant,
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain(cssClass);
- });
- },
- );
-
- it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
- createComponent();
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
- });
-
- it('renders `dropdown-title` component', async () => {
- createComponent();
- await wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownTitle).exists()).toBe(true);
- });
-
- it('renders `dropdown-value` component', async () => {
- createComponent(mockConfig, {
- default: 'None',
+ it.each`
+ variant | cssClass
+ ${'standalone'} | ${'is-standalone'}
+ ${'embedded'} | ${'is-embedded'}
+ `(
+ 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
+ ({ variant, cssClass }) => {
+ createComponent({
+ ...mockConfig,
+ variant,
});
- await wrapper.vm.$nextTick;
-
- const valueComp = wrapper.find(DropdownValue);
-
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
- });
-
- it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => {
- createComponent();
- wrapper.vm.$store.dispatch('toggleDropdownButton');
- await wrapper.vm.$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 wrapper.vm.$nextTick;
- expect(wrapper.find(DropdownContents).exists()).toBe(true);
- });
- describe('sets content direction based on viewport', () => {
- describe.each(Object.values(DropdownVariant))(
- 'when labels variant is "%s"',
- ({ variant }) => {
- beforeEach(() => {
- createComponent({ ...mockConfig, variant });
- wrapper.vm.$store.dispatch('toggleDropdownContents');
- });
-
- it('set direction when out of viewport', () => {
- isInViewport.mockImplementation(() => false);
- wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
- });
- });
-
- it('does not set direction when inside of viewport', () => {
- isInViewport.mockImplementation(() => true);
- wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
- });
- });
- },
- );
- });
- });
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain(cssClass);
+ });
+ },
+ );
- it('calls toggleDropdownContents action when isEditing prop is changing to true', async () => {
+ it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => {
createComponent();
-
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- await wrapper.setProps({ isEditing: true });
-
- expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContents');
+ await wrapper.vm.$nextTick;
+ expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
- it('does not call toggleDropdownContents action when isEditing prop is changing to false', async () => {
- createComponent();
+ it('renders `dropdown-value` component', async () => {
+ createComponent(mockConfig, {
+ default: 'None',
+ });
+ await wrapper.vm.$nextTick;
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- await wrapper.setProps({ isEditing: false });
+ const valueComp = wrapper.find(DropdownValue);
- expect(store.dispatch).not.toHaveBeenCalled();
+ expect(valueComp.exists()).toBe(true);
+ expect(valueComp.text()).toBe('None');
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 5dd8fc1b8b2..fceaabec2d0 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -34,18 +34,12 @@ export const mockLabels = [
];
export const mockConfig = {
- allowLabelEdit: true,
- allowLabelCreate: true,
- allowScopedLabels: true,
allowMultiselect: true,
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
variant: 'sidebar',
- dropdownOnly: false,
selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false,
- labelsFetchPath: '/gitlab-org/my-project/-/labels.json',
- labelsManagePath: '/gitlab-org/my-project/-/labels',
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
footerCreateLabelTitle: 'create',
@@ -83,9 +77,7 @@ export const createLabelSuccessfulResponse = {
id: 'gid://gitlab/ProjectLabel/126',
color: '#dc143c',
description: null,
- descriptionHtml: '',
title: 'ewrwrwer',
- textColor: '#FFFFFF',
__typename: 'Label',
},
errors: [],
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js
deleted file mode 100644
index ee905410ffa..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/actions_spec.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import testAction from 'helpers/vuex_action_helper';
-import * as actions from '~/vue_shared/components/sidebar/labels_select_widget/store/actions';
-import * as types from '~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types';
-import defaultState from '~/vue_shared/components/sidebar/labels_select_widget/store/state';
-
-jest.mock('~/flash');
-
-describe('LabelsSelect Actions', () => {
- let state;
- const mockInitialState = {
- labels: [],
- selectedLabels: [],
- };
-
- beforeEach(() => {
- state = { ...defaultState() };
- });
-
- describe('setInitialState', () => {
- it('sets initial store state', (done) => {
- testAction(
- actions.setInitialState,
- mockInitialState,
- state,
- [{ type: types.SET_INITIAL_STATE, payload: mockInitialState }],
- [],
- done,
- );
- });
- });
-
- describe('toggleDropdownButton', () => {
- it('toggles dropdown button', (done) => {
- testAction(
- actions.toggleDropdownButton,
- {},
- state,
- [{ type: types.TOGGLE_DROPDOWN_BUTTON }],
- [],
- done,
- );
- });
- });
-
- describe('toggleDropdownContents', () => {
- it('toggles dropdown contents', (done) => {
- testAction(
- actions.toggleDropdownContents,
- {},
- state,
- [{ type: types.TOGGLE_DROPDOWN_CONTENTS }],
- [],
- done,
- );
- });
- });
-
- describe('toggleDropdownContentsCreateView', () => {
- it('toggles dropdown create view', (done) => {
- testAction(
- actions.toggleDropdownContentsCreateView,
- {},
- state,
- [{ type: types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW }],
- [],
- done,
- );
- });
- });
-
- describe('updateSelectedLabels', () => {
- it('updates `state.labels` based on provided `labels` param', (done) => {
- const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
-
- testAction(
- actions.updateSelectedLabels,
- labels,
- state,
- [{ type: types.UPDATE_SELECTED_LABELS, payload: { labels } }],
- [],
- done,
- );
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/getters_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/getters_spec.js
deleted file mode 100644
index 40eb0323146..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/getters_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import * as getters from '~/vue_shared/components/sidebar/labels_select_widget/store/getters';
-
-describe('LabelsSelect Getters', () => {
- describe('dropdownButtonText', () => {
- it.each`
- labelType | dropdownButtonText | expected
- ${'default'} | ${''} | ${'Label'}
- ${'custom'} | ${'Custom label'} | ${'Custom label'}
- `(
- 'returns $labelType text when state.labels has no selected labels',
- ({ dropdownButtonText, expected }) => {
- const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
- const selectedLabels = [];
- const state = { labels, selectedLabels, dropdownButtonText };
-
- expect(getters.dropdownButtonText(state, {})).toBe(expected);
- },
- );
-
- 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',
- );
- });
-
- 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',
- );
- });
- });
-
- describe('selectedLabelsList', () => {
- it('returns array of IDs of all labels within `state.selectedLabels`', () => {
- const selectedLabels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
-
- expect(getters.selectedLabelsList({ selectedLabels })).toEqual([1, 2, 3, 4]);
- });
- });
-
- describe('isDropdownVariantSidebar', () => {
- it('returns `true` when `state.variant` is "sidebar"', () => {
- expect(getters.isDropdownVariantSidebar({ variant: 'sidebar' })).toBe(true);
- });
- });
-
- describe('isDropdownVariantStandalone', () => {
- it('returns `true` when `state.variant` is "standalone"', () => {
- expect(getters.isDropdownVariantStandalone({ variant: 'standalone' })).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js
deleted file mode 100644
index 1f0e0eee420..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/store/mutations_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import * as types from '~/vue_shared/components/sidebar/labels_select_widget/store/mutation_types';
-import mutations from '~/vue_shared/components/sidebar/labels_select_widget/store/mutations';
-
-describe('LabelsSelect Mutations', () => {
- describe(`${types.SET_INITIAL_STATE}`, () => {
- it('initializes provided props to store state', () => {
- const state = {};
- mutations[types.SET_INITIAL_STATE](state, {
- labels: 'foo',
- });
-
- expect(state.labels).toEqual('foo');
- });
- });
-
- describe(`${types.TOGGLE_DROPDOWN_BUTTON}`, () => {
- it('toggles value of `state.showDropdownButton`', () => {
- const state = {
- showDropdownButton: false,
- };
- mutations[types.TOGGLE_DROPDOWN_BUTTON](state);
-
- expect(state.showDropdownButton).toBe(true);
- });
- });
-
- describe(`${types.TOGGLE_DROPDOWN_CONTENTS}`, () => {
- it('toggles value of `state.showDropdownButton` when `state.dropdownOnly` is false', () => {
- const state = {
- dropdownOnly: false,
- showDropdownButton: false,
- variant: 'sidebar',
- };
- mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
-
- expect(state.showDropdownButton).toBe(true);
- });
-
- it('toggles value of `state.showDropdownContents`', () => {
- const state = {
- showDropdownContents: false,
- };
- mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
-
- expect(state.showDropdownContents).toBe(true);
- });
-
- it('sets value of `state.showDropdownContentsCreateView` to `false` when `showDropdownContents` is true', () => {
- const state = {
- showDropdownContents: false,
- showDropdownContentsCreateView: true,
- };
- mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
-
- expect(state.showDropdownContentsCreateView).toBe(false);
- });
- });
-
- describe(`${types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW}`, () => {
- it('toggles value of `state.showDropdownContentsCreateView`', () => {
- const state = {
- showDropdownContentsCreateView: false,
- };
- mutations[types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state);
-
- expect(state.showDropdownContentsCreateView).toBe(true);
- });
- });
-
- describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
- let labels;
-
- beforeEach(() => {
- labels = [
- { id: 1, title: 'scoped::test', set: true },
- { id: 2, set: false, title: 'scoped::one' },
- { id: 3, title: '' },
- { id: 4, title: '' },
- ];
- });
-
- it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
- const updatedLabelIds = [2];
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
-
- state.labels.forEach((label) => {
- if (updatedLabelIds.includes(label.id)) {
- expect(label.touched).toBe(true);
- expect(label.set).toBe(true);
- }
- });
- });
-
- describe('when label is scoped', () => {
- it('unsets the currently selected scoped label and sets the current label', () => {
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, {
- labels: [{ id: 2, title: 'scoped::one' }],
- });
-
- expect(state.labels).toEqual([
- { id: 1, title: 'scoped::test', set: false },
- { id: 2, set: true, title: 'scoped::one', touched: true },
- { id: 3, title: '' },
- { id: 4, title: '' },
- ]);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js b/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js
new file mode 100644
index 00000000000..103eee4b9a8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/storage_counter/usage_graph_spec.js
@@ -0,0 +1,137 @@
+import { shallowMount } from '@vue/test-utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
+
+let data;
+let wrapper;
+
+function mountComponent({ rootStorageStatistics, limit }) {
+ wrapper = shallowMount(UsageGraph, {
+ propsData: {
+ rootStorageStatistics,
+ limit,
+ },
+ });
+}
+function findStorageTypeUsagesSerialized() {
+ return wrapper
+ .findAll('[data-testid="storage-type-usage"]')
+ .wrappers.map((wp) => wp.element.style.flex);
+}
+
+describe('Storage Counter usage graph component', () => {
+ beforeEach(() => {
+ data = {
+ rootStorageStatistics: {
+ wikiSize: 5000,
+ repositorySize: 4000,
+ packagesSize: 3000,
+ lfsObjectsSize: 2000,
+ buildArtifactsSize: 500,
+ pipelineArtifactsSize: 500,
+ snippetsSize: 2000,
+ storageSize: 17000,
+ uploadsSize: 1000,
+ },
+ limit: 2000,
+ };
+ mountComponent(data);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the legend in order', () => {
+ const types = wrapper.findAll('[data-testid="storage-type-legend"]');
+
+ const {
+ buildArtifactsSize,
+ pipelineArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ repositorySize,
+ wikiSize,
+ snippetsSize,
+ uploadsSize,
+ } = data.rootStorageStatistics;
+
+ expect(types.at(0).text()).toMatchInterpolatedText(`Wikis ${numberToHumanSize(wikiSize)}`);
+ expect(types.at(1).text()).toMatchInterpolatedText(
+ `Repositories ${numberToHumanSize(repositorySize)}`,
+ );
+ expect(types.at(2).text()).toMatchInterpolatedText(
+ `Packages ${numberToHumanSize(packagesSize)}`,
+ );
+ expect(types.at(3).text()).toMatchInterpolatedText(
+ `LFS Objects ${numberToHumanSize(lfsObjectsSize)}`,
+ );
+ expect(types.at(4).text()).toMatchInterpolatedText(
+ `Snippets ${numberToHumanSize(snippetsSize)}`,
+ );
+ expect(types.at(5).text()).toMatchInterpolatedText(
+ `Artifacts ${numberToHumanSize(buildArtifactsSize + pipelineArtifactsSize)}`,
+ );
+ expect(types.at(6).text()).toMatchInterpolatedText(`Uploads ${numberToHumanSize(uploadsSize)}`);
+ });
+
+ describe('when storage type is not used', () => {
+ beforeEach(() => {
+ data.rootStorageStatistics.wikiSize = 0;
+ mountComponent(data);
+ });
+
+ it('filters the storage type', () => {
+ expect(wrapper.text()).not.toContain('Wikis');
+ });
+ });
+
+ describe('when there is no storage usage', () => {
+ beforeEach(() => {
+ data.rootStorageStatistics.storageSize = 0;
+ mountComponent(data);
+ });
+
+ it('it does not render', () => {
+ expect(wrapper.html()).toEqual('');
+ });
+ });
+
+ describe('when limit is 0', () => {
+ beforeEach(() => {
+ data.limit = 0;
+ mountComponent(data);
+ });
+
+ it('sets correct flex values', () => {
+ expect(findStorageTypeUsagesSerialized()).toStrictEqual([
+ '0.29411764705882354',
+ '0.23529411764705882',
+ '0.17647058823529413',
+ '0.11764705882352941',
+ '0.11764705882352941',
+ '0.058823529411764705',
+ '0.058823529411764705',
+ ]);
+ });
+ });
+
+ describe('when storage exceeds limit', () => {
+ beforeEach(() => {
+ data.limit = data.rootStorageStatistics.storageSize - 1;
+ mountComponent(data);
+ });
+
+ it('it does render correclty', () => {
+ expect(findStorageTypeUsagesSerialized()).toStrictEqual([
+ '0.29411764705882354',
+ '0.23529411764705882',
+ '0.17647058823529413',
+ '0.11764705882352941',
+ '0.11764705882352941',
+ '0.058823529411764705',
+ '0.058823529411764705',
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 538e67ef354..926223e0670 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -94,7 +94,7 @@ describe('User Popover Component', () => {
const bio = 'My super interesting bio';
it('should show only bio if work information is not available', () => {
- const user = { ...DEFAULT_PROPS.user, bio, bioHtml: bio };
+ const user = { ...DEFAULT_PROPS.user, bio };
createWrapper({ user });
@@ -117,7 +117,6 @@ describe('User Popover Component', () => {
const user = {
...DEFAULT_PROPS.user,
bio,
- bioHtml: bio,
workInformation: 'Frontend Engineer at GitLab',
};
@@ -127,16 +126,15 @@ describe('User Popover Component', () => {
expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab');
});
- it('should not encode special characters in bio', () => {
+ it('should encode special characters in bio', () => {
const user = {
...DEFAULT_PROPS.user,
- bio: 'I like CSS',
- bioHtml: 'I like <b>CSS</b>',
+ bio: 'I like <b>CSS</b>',
};
createWrapper({ user });
- expect(findBio().html()).toContain('I like <b>CSS</b>');
+ expect(findBio().html()).toContain('I like &lt;b&gt;CSS&lt;/b&gt;');
});
it('shows icon for bio', () => {
@@ -250,6 +248,13 @@ describe('User Popover Component', () => {
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.exists()).toBe(true);
expect(securityBotDocsLink.attributes('href')).toBe(SECURITY_BOT_USER.websiteUrl);
+ expect(securityBotDocsLink.text()).toBe('Learn more about GitLab Security Bot');
+ });
+
+ it("doesn't escape user's name", () => {
+ createWrapper({ user: { ...SECURITY_BOT_USER, name: '%<>\';"' } });
+ const securityBotDocsLink = findSecurityBotDocsLink();
+ expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"');
});
});
});
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index bf4b57d8afb..13f221fd9d9 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import Dropzone from 'dropzone';
import $ from 'jquery';
import Mousetrap from 'mousetrap';
-import initNotes from '~/init_notes';
+import GLForm from '~/gl_form';
import * as utils from '~/lib/utils/common_utils';
import ZenMode from '~/zen_mode';
@@ -34,7 +34,9 @@ describe('ZenMode', () => {
mock.onGet().reply(200);
loadFixtures(fixtureName);
- initNotes();
+
+ const form = $('.js-new-note-form');
+ new GLForm(form); // eslint-disable-line no-new
dropzoneForElementSpy = jest.spyOn(Dropzone, 'forElement').mockImplementation(() => ({
enable: () => true,