From 983a0bba5d2a042c4a3bbb22432ec192c7501d82 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Apr 2020 18:38:24 +0000 Subject: Add latest changes from gitlab-org/gitlab@12-10-stable-ee --- .../admin/integrations_controller_spec.rb | 9 +- spec/controllers/admin/runners_controller_spec.rb | 24 + spec/controllers/admin/users_controller_spec.rb | 11 + .../settings/integrations_controller_spec.rb | 7 +- .../controllers/projects/issues_controller_spec.rb | 39 + .../projects/merge_requests_controller_spec.rb | 4 +- .../repositories/git_http_controller_spec.rb | 38 +- spec/factories/ci/bridge.rb | 16 +- spec/factories/ci/job_artifacts.rb | 2 +- spec/factories/clusters/applications/helm.rb | 9 + spec/factories/diff_position.rb | 10 + spec/factories/import_failures.rb | 23 + spec/factories/projects.rb | 4 + spec/factories/terraform/state.rb | 4 +- spec/factories/users.rb | 9 + spec/features/admin/admin_mode/workers_spec.rb | 8 - spec/features/admin/admin_settings_spec.rb | 15 +- spec/features/cycle_analytics_spec.rb | 12 +- spec/features/issues/csv_spec.rb | 100 + .../merge_request/user_resolves_wip_mr_spec.rb | 48 + .../projects/environments_pod_logs_spec.rb | 4 +- .../projects/snippets/create_snippet_spec.rb | 1 + .../projects/snippets/user_updates_snippet_spec.rb | 1 + spec/features/snippets/spam_snippets_spec.rb | 1 + .../features/snippets/user_creates_snippet_spec.rb | 1 + spec/features/snippets/user_edits_snippet_spec.rb | 1 + spec/features/static_site_editor_spec.rb | 19 + .../autocomplete/move_to_project_finder_spec.rb | 13 +- .../metrics/dashboards/annotations_finder_spec.rb | 107 + spec/fixtures/api/schemas/cluster_status.json | 3 + spec/fixtures/lib/elasticsearch/pods_query.json | 28 + spec/fixtures/lib/elasticsearch/pods_response.json | 75 + .../blob/components/blob_edit_content_spec.js | 2 +- .../components/ci_key_field_spec.js | 244 ++ .../components/ci_variable_modal_spec.js | 153 +- .../services/application_state_machine_spec.js | 16 + spec/frontend/diffs/components/commit_item_spec.js | 4 +- .../diffs/components/diff_table_cell_spec.js | 15 +- spec/frontend/diffs/store/actions_spec.js | 1 + .../diffs/store/getters_versions_dropdowns_spec.js | 99 +- spec/frontend/diffs/store/mutations_spec.js | 67 + spec/frontend/fixtures/merge_requests_diffs.rb | 7 +- spec/frontend/helpers/dom_events_helper.js | 10 + .../jira_import/components/jira_import_app_spec.js | 207 +- .../components/jira_import_form_spec.js | 136 +- .../components/jira_import_progress_spec.js | 70 + .../components/jira_import_setup_spec.js | 17 +- spec/frontend/jira_import/utils_spec.js | 27 + spec/frontend/logs/mock_data.js | 76 +- .../__snapshots__/dashboard_template_spec.js.snap | 169 +- .../components/charts/annotations_spec.js | 11 +- .../monitoring/components/charts/options_spec.js | 29 +- .../components/charts/time_series_spec.js | 55 +- .../monitoring/components/dashboard_spec.js | 132 +- .../components/dashboard_template_spec.js | 2 +- .../components/dashboard_url_time_spec.js | 3 +- .../monitoring/components/panel_type_spec.js | 93 +- spec/frontend/monitoring/fixture_data.js | 49 + spec/frontend/monitoring/init_utils.js | 57 - spec/frontend/monitoring/mock_data.js | 348 +- spec/frontend/monitoring/store/actions_spec.js | 75 +- spec/frontend/monitoring/store/getters_spec.js | 90 +- spec/frontend/monitoring/store/mutations_spec.js | 7 +- spec/frontend/monitoring/store/utils_spec.js | 31 +- spec/frontend/monitoring/store_utils.js | 34 + spec/frontend/monitoring/utils_spec.js | 11 +- .../permissions/components/settings_panel_spec.js | 31 +- .../pipelines/graph/action_component_spec.js | 9 +- .../pipelines/graph/graph_component_spec.js | 305 ++ .../pipelines/graph/job_group_dropdown_spec.js | 84 + spec/frontend/pipelines/graph/job_item_spec.js | 8 +- .../pipelines/graph/job_name_component_spec.js | 36 + .../pipelines/graph/linked_pipeline_spec.js | 24 +- .../graph/linked_pipelines_column_spec.js | 38 + .../pipelines/graph/linked_pipelines_mock_data.js | 4084 ++++++++++++++++++-- spec/frontend/pipelines/graph/mock_data.js | 261 ++ .../pipelines/graph/stage_column_component_spec.js | 136 + spec/frontend/registry/explorer/pages/list_spec.js | 49 +- .../registry/explorer/stores/actions_spec.js | 29 +- .../registry/explorer/stores/mutations_spec.js | 22 +- spec/frontend/repository/router_spec.js | 17 +- spec/frontend/sidebar/sidebar_assignees_spec.js | 74 + spec/frontend/snippet/snippet_edit_spec.js | 45 + .../snippet_description_edit_spec.js.snap | 1 - spec/frontend/snippets/components/edit_spec.js | 279 ++ .../snippets/components/snippet_header_spec.js | 10 +- .../components/invalid_content_message_spec.js | 23 + .../components/publish_toolbar_spec.js | 4 +- .../components/saved_changes_message_spec.js | 28 +- .../components/static_site_editor_spec.js | 79 +- .../components/submit_changes_error_spec.js | 48 + spec/frontend/static_site_editor/mock_data.js | 4 +- .../static_site_editor/store/actions_spec.js | 19 +- .../static_site_editor/store/mutations_spec.js | 29 +- .../__snapshots__/awards_list_spec.js.snap | 287 ++ .../__snapshots__/clone_dropdown_spec.js.snap | 8 +- .../vue_shared/components/awards_list_spec.js | 213 + .../form/__snapshots__/title_spec.js.snap | 4 +- .../components/user_popover/user_popover_spec.js | 73 +- .../metrics/dashboards/annotation_resolver_spec.rb | 60 + spec/graphql/types/metrics/dashboard_type_spec.rb | 11 +- .../metrics/dashboards/annotation_type_spec.rb | 17 + spec/initializers/lograge_spec.rb | 4 +- .../filtered_search_manager_spec.js | 102 +- .../monitoring/components/dashboard_resize_spec.js | 61 +- spec/javascripts/monitoring/fixture_data.js | 1 + spec/javascripts/monitoring/store_utils.js | 1 + .../pipelines/graph/graph_component_spec.js | 274 -- .../pipelines/graph/job_group_dropdown_spec.js | 85 - .../pipelines/graph/job_name_component_spec.js | 27 - .../graph/linked_pipelines_column_spec.js | 43 - .../pipelines/graph/linked_pipelines_mock_data.js | 3 - spec/javascripts/pipelines/graph/mock_data.js | 261 -- .../pipelines/graph/stage_column_component_spec.js | 122 - spec/javascripts/sidebar/sidebar_assignees_spec.js | 64 - .../project_import_failed_relation_spec.rb | 23 + .../lib/api/entities/project_import_status_spec.rb | 49 + spec/lib/api/entities/user_spec.rb | 26 + spec/lib/api/validations/validators/limit_spec.rb | 25 + spec/lib/banzai/pipeline_spec.rb | 64 + spec/lib/csv_builder_spec.rb | 109 + spec/lib/gitlab/application_context_spec.rb | 12 + .../create_resource_user_mention_spec.rb | 2 +- spec/lib/gitlab/ci/jwt_spec.rb | 124 + spec/lib/gitlab/ci/status/bridge/factory_spec.rb | 72 + spec/lib/gitlab/current_settings_spec.rb | 27 +- .../cycle_analytics/group_stage_summary_spec.rb | 66 +- .../gitlab/cycle_analytics/stage_summary_spec.rb | 90 +- spec/lib/gitlab/data_builder/pipeline_spec.rb | 2 +- spec/lib/gitlab/database/migration_helpers_spec.rb | 6 + .../gitlab/diff/formatters/text_formatter_spec.rb | 3 +- spec/lib/gitlab/diff/highlight_cache_spec.rb | 52 +- spec/lib/gitlab/diff/position_spec.rb | 1 + spec/lib/gitlab/elasticsearch/logs/lines_spec.rb | 89 + spec/lib/gitlab/elasticsearch/logs/pods_spec.rb | 35 + spec/lib/gitlab/elasticsearch/logs_spec.rb | 89 - spec/lib/gitlab/file_hook_spec.rb | 2 +- spec/lib/gitlab/gitaly_client_spec.rb | 9 + .../grape_logging/loggers/perf_logger_spec.rb | 2 +- .../loggers/queue_duration_logger_spec.rb | 4 +- .../group/legacy_tree_restorer_spec.rb | 153 + .../import_export/group/tree_restorer_spec.rb | 153 - .../import_export/project/import_task_spec.rb | 2 +- .../import_export/project/tree_restorer_spec.rb | 2 +- .../gitlab/import_export/safe_model_attributes.yml | 2 + spec/lib/gitlab/instrumentation_helper_spec.rb | 10 +- spec/lib/gitlab/json_spec.rb | 91 + .../gitlab/kubernetes/helm/base_command_spec.rb | 52 + .../gitlab/kubernetes/helm/init_command_spec.rb | 52 - .../gitlab/kubernetes/helm/install_command_spec.rb | 16 - .../gitlab/kubernetes/helm/patch_command_spec.rb | 16 - spec/lib/gitlab/project_template_spec.rb | 1 + spec/lib/gitlab/prometheus/adapter_spec.rb | 8 + .../sidekiq_logging/structured_logger_spec.rb | 18 +- .../duplicate_jobs/server_spec.rb | 13 +- .../worker_context/server_spec.rb | 13 +- spec/lib/gitlab/sidekiq_middleware_spec.rb | 47 +- .../slash_commands/presenters/issue_show_spec.rb | 4 +- spec/lib/gitlab/utils_spec.rb | 18 +- spec/lib/marginalia_spec.rb | 30 +- spec/mailers/emails/issues_spec.rb | 49 + .../cleanup_empty_commit_user_mentions_spec.rb | 2 +- .../migrate_commit_notes_mentions_to_db_spec.rb | 2 +- spec/models/ci/bridge_spec.rb | 2 - spec/models/ci/build_spec.rb | 108 +- spec/models/ci/job_artifact_spec.rb | 21 +- spec/models/ci/processable_spec.rb | 78 + spec/models/ci/runner_spec.rb | 30 + spec/models/clusters/applications/fluentd_spec.rb | 50 + spec/models/clusters/applications/ingress_spec.rb | 6 + spec/models/clusters/cluster_spec.rb | 3 +- spec/models/concerns/issuable_spec.rb | 34 + spec/models/cycle_analytics/group_level_spec.rb | 2 +- spec/models/diff_note_position_spec.rb | 7 + spec/models/import_failure_spec.rb | 23 +- spec/models/jira_import_state_spec.rb | 20 +- spec/models/merge_request_diff_spec.rb | 39 + spec/models/merge_request_spec.rb | 66 +- spec/models/metrics/dashboard/annotation_spec.rb | 26 + spec/models/project_feature_spec.rb | 4 +- spec/models/project_import_state_spec.rb | 21 +- .../project_services/prometheus_service_spec.rb | 44 + spec/models/project_spec.rb | 40 +- spec/models/resource_milestone_event_spec.rb | 26 + spec/models/terraform/state_spec.rb | 25 +- spec/models/user_spec.rb | 44 +- spec/models/user_type_enums_spec.rb | 13 + spec/policies/global_policy_spec.rb | 33 + spec/requests/api/deploy_tokens_spec.rb | 7 +- .../graphql/metrics/dashboard/annotations_spec.rb | 109 + .../graphql/mutations/jira_import/start_spec.rb | 12 +- .../api/graphql/project/merge_request_spec.rb | 11 + spec/requests/api/markdown_spec.rb | 2 - spec/requests/api/merge_requests_spec.rb | 24 + spec/requests/api/project_statistics_spec.rb | 8 - spec/requests/api/projects_spec.rb | 4 +- spec/requests/api/terraform/state_spec.rb | 238 +- spec/routing/openid_connect_spec.rb | 5 + spec/routing/project_routing_spec.rb | 7 + .../cop/rspec/modify_sidekiq_middleware_spec.rb | 39 + .../cop/static_translation_definition_spec.rb | 109 + .../analytics_summary_serializer_spec.rb | 5 +- spec/serializers/discussion_entity_spec.rb | 10 + .../serializers/merge_request_basic_entity_spec.rb | 17 + ...merge_request_poll_cached_widget_entity_spec.rb | 6 + .../merge_request_poll_widget_entity_spec.rb | 4 + spec/serializers/merge_request_serializer_spec.rb | 16 + .../merge_when_pipeline_succeeds_service_spec.rb | 22 +- spec/services/auto_merge_service_spec.rb | 69 +- .../create_cross_project_pipeline_service_spec.rb | 40 + spec/services/ci/update_runner_service_spec.rb | 13 + spec/services/emails/destroy_service_spec.rb | 5 +- .../git/process_ref_changes_service_spec.rb | 43 + spec/services/issues/export_csv_service_spec.rb | 170 + .../jira_import/start_import_service_spec.rb | 35 +- .../merge_orchestration_service_spec.rb | 116 + .../merge_requests/pushed_branches_service_spec.rb | 42 + .../services/merge_requests/update_service_spec.rb | 14 +- .../dashboard/transient_embed_service_spec.rb | 50 +- .../personal_access_tokens/create_service_spec.rb | 24 + spec/services/pod_logs/base_service_spec.rb | 27 +- .../pod_logs/elasticsearch_service_spec.rb | 63 +- spec/services/pod_logs/kubernetes_service_spec.rb | 32 +- .../quick_actions/interpret_service_spec.rb | 29 +- .../resources/create_access_token_service_spec.rb | 163 + spec/services/snippets/create_service_spec.rb | 37 + .../terraform/remote_state_handler_spec.rb | 143 + spec/services/users/build_service_spec.rb | 20 + .../x509_certificate_revoke_service_spec.rb | 2 - spec/spec_helper.rb | 17 + spec/support/helpers/api_helpers.rb | 11 + spec/support/helpers/migrations_helpers.rb | 3 + spec/support/import_export/configuration_helper.rb | 4 +- spec/support/matchers/exclude_matcher.rb | 3 + .../controllers/deploy_token_shared_examples.rb | 14 +- .../diff_positionable_note_shared_examples.rb | 1 + .../merge_quick_action_shared_examples.rb | 19 +- .../api/diff_discussions_shared_examples.rb | 8 +- spec/support/sidekiq_middleware.rb | 16 +- spec/uploaders/records_uploads_spec.rb | 6 +- spec/uploaders/terraform/state_uploader_spec.rb | 6 +- .../shared/projects/_project.html.haml_spec.rb | 2 +- spec/workers/concerns/cronjob_queue_spec.rb | 22 + .../workers/create_commit_signature_worker_spec.rb | 59 +- spec/workers/expire_pipeline_cache_worker_spec.rb | 8 +- spec/workers/export_csv_worker_spec.rb | 34 + .../jira_import/stage/finish_import_worker_spec.rb | 18 +- spec/workers/post_receive_spec.rb | 3 + 248 files changed, 11916 insertions(+), 3122 deletions(-) create mode 100644 spec/factories/import_failures.rb create mode 100644 spec/features/issues/csv_spec.rb create mode 100644 spec/features/merge_request/user_resolves_wip_mr_spec.rb create mode 100644 spec/features/static_site_editor_spec.rb create mode 100644 spec/finders/metrics/dashboards/annotations_finder_spec.rb create mode 100644 spec/fixtures/lib/elasticsearch/pods_query.json create mode 100644 spec/fixtures/lib/elasticsearch/pods_response.json create mode 100644 spec/frontend/ci_variable_list/components/ci_key_field_spec.js create mode 100644 spec/frontend/helpers/dom_events_helper.js create mode 100644 spec/frontend/jira_import/components/jira_import_progress_spec.js create mode 100644 spec/frontend/jira_import/utils_spec.js create mode 100644 spec/frontend/monitoring/fixture_data.js delete mode 100644 spec/frontend/monitoring/init_utils.js create mode 100644 spec/frontend/monitoring/store_utils.js create mode 100644 spec/frontend/pipelines/graph/graph_component_spec.js create mode 100644 spec/frontend/pipelines/graph/job_group_dropdown_spec.js create mode 100644 spec/frontend/pipelines/graph/job_name_component_spec.js create mode 100644 spec/frontend/pipelines/graph/linked_pipelines_column_spec.js create mode 100644 spec/frontend/pipelines/graph/mock_data.js create mode 100644 spec/frontend/pipelines/graph/stage_column_component_spec.js create mode 100644 spec/frontend/sidebar/sidebar_assignees_spec.js create mode 100644 spec/frontend/snippet/snippet_edit_spec.js create mode 100644 spec/frontend/snippets/components/edit_spec.js create mode 100644 spec/frontend/static_site_editor/components/invalid_content_message_spec.js create mode 100644 spec/frontend/static_site_editor/components/submit_changes_error_spec.js create mode 100644 spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/awards_list_spec.js create mode 100644 spec/graphql/resolvers/metrics/dashboards/annotation_resolver_spec.rb create mode 100644 spec/graphql/types/metrics/dashboards/annotation_type_spec.rb create mode 100644 spec/javascripts/monitoring/fixture_data.js create mode 100644 spec/javascripts/monitoring/store_utils.js delete mode 100644 spec/javascripts/pipelines/graph/graph_component_spec.js delete mode 100644 spec/javascripts/pipelines/graph/job_group_dropdown_spec.js delete mode 100644 spec/javascripts/pipelines/graph/job_name_component_spec.js delete mode 100644 spec/javascripts/pipelines/graph/linked_pipelines_column_spec.js delete mode 100644 spec/javascripts/pipelines/graph/linked_pipelines_mock_data.js delete mode 100644 spec/javascripts/pipelines/graph/mock_data.js delete mode 100644 spec/javascripts/pipelines/graph/stage_column_component_spec.js delete mode 100644 spec/javascripts/sidebar/sidebar_assignees_spec.js create mode 100644 spec/lib/api/entities/project_import_failed_relation_spec.rb create mode 100644 spec/lib/api/entities/project_import_status_spec.rb create mode 100644 spec/lib/api/entities/user_spec.rb create mode 100644 spec/lib/api/validations/validators/limit_spec.rb create mode 100644 spec/lib/banzai/pipeline_spec.rb create mode 100644 spec/lib/csv_builder_spec.rb create mode 100644 spec/lib/gitlab/ci/jwt_spec.rb create mode 100644 spec/lib/gitlab/ci/status/bridge/factory_spec.rb create mode 100644 spec/lib/gitlab/elasticsearch/logs/lines_spec.rb create mode 100644 spec/lib/gitlab/elasticsearch/logs/pods_spec.rb delete mode 100644 spec/lib/gitlab/elasticsearch/logs_spec.rb create mode 100644 spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb delete mode 100644 spec/lib/gitlab/import_export/group/tree_restorer_spec.rb create mode 100644 spec/lib/gitlab/json_spec.rb create mode 100644 spec/models/clusters/applications/fluentd_spec.rb create mode 100644 spec/models/user_type_enums_spec.rb create mode 100644 spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb create mode 100644 spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb create mode 100644 spec/rubocop/cop/static_translation_definition_spec.rb create mode 100644 spec/serializers/merge_request_basic_entity_spec.rb create mode 100644 spec/services/issues/export_csv_service_spec.rb create mode 100644 spec/services/merge_requests/merge_orchestration_service_spec.rb create mode 100644 spec/services/merge_requests/pushed_branches_service_spec.rb create mode 100644 spec/services/personal_access_tokens/create_service_spec.rb create mode 100644 spec/services/resources/create_access_token_service_spec.rb create mode 100644 spec/services/terraform/remote_state_handler_spec.rb create mode 100644 spec/support/matchers/exclude_matcher.rb create mode 100644 spec/workers/export_csv_worker_spec.rb (limited to 'spec') diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb index 8e48ecddd0f..817223bd91a 100644 --- a/spec/controllers/admin/integrations_controller_spec.rb +++ b/spec/controllers/admin/integrations_controller_spec.rb @@ -49,11 +49,12 @@ describe Admin::IntegrationsController do end context 'invalid params' do - let(:url) { 'https://jira.localhost' } + let(:url) { 'invalid' } - it 'updates the integration' do - expect(response).to have_gitlab_http_status(:found) - expect(integration.reload.url).to eq(url) + it 'does not update the integration' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + expect(integration.reload.url).not_to eq(url) end end end diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb index 7582006df36..803fcf90135 100644 --- a/spec/controllers/admin/runners_controller_spec.rb +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -72,6 +72,30 @@ describe Admin::RunnersController do expect(response).to have_gitlab_http_status(:ok) end + + describe 'Cost factors values' do + context 'when it is Gitlab.com' do + before do + expect(Gitlab).to receive(:com?).at_least(:once) { true } + end + + it 'renders cost factors fields' do + get :show, params: { id: runner.id } + + expect(response.body).to match /Private projects Minutes cost factor/ + expect(response.body).to match /Public projects Minutes cost factor/ + end + end + + context 'when it is not Gitlab.com' do + it 'does not show cost factor fields' do + get :show, params: { id: runner.id } + + expect(response.body).not_to match /Private projects Minutes cost factor/ + expect(response.body).not_to match /Public projects Minutes cost factor/ + end + end + end end describe '#update' do diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index a4ce510b413..387fc0407b6 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -340,6 +340,17 @@ describe Admin::UsersController do end end + describe "DELETE #remove_email" do + it 'deletes the email' do + email = create(:email, user: user) + + delete :remove_email, params: { id: user.username, email_id: email.id } + + expect(user.reload.emails).not_to include(email) + expect(flash[:notice]).to eq('Successfully removed email.') + end + end + describe "POST impersonate" do context "when the user is blocked" do before do diff --git a/spec/controllers/groups/settings/integrations_controller_spec.rb b/spec/controllers/groups/settings/integrations_controller_spec.rb index 6df1ad8a383..76cd74de183 100644 --- a/spec/controllers/groups/settings/integrations_controller_spec.rb +++ b/spec/controllers/groups/settings/integrations_controller_spec.rb @@ -100,11 +100,12 @@ describe Groups::Settings::IntegrationsController do end context 'invalid params' do - let(:url) { 'https://jira.localhost' } + let(:url) { 'invalid' } it 'does not update the integration' do - expect(response).to have_gitlab_http_status(:found) - expect(integration.reload.url).to eq(url) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + expect(integration.reload.url).not_to eq(url) end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 9526e14a748..862a4bd3559 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1427,6 +1427,45 @@ describe Projects::IssuesController do end end + describe 'POST export_csv' do + let(:viewer) { user } + let(:issue) { create(:issue, project: project) } + + before do + project.add_developer(user) + end + + def request_csv + post :export_csv, params: { namespace_id: project.namespace.to_param, project_id: project.to_param } + end + + context 'when logged in' do + before do + sign_in(viewer) + end + + it 'allows CSV export' do + expect(ExportCsvWorker).to receive(:perform_async).with(viewer.id, project.id, anything) + + request_csv + + expect(response).to redirect_to(project_issues_path(project)) + expect(response.flash[:notice]).to match(/\AYour CSV export has started/i) + end + end + + context 'when not logged in' do + let(:project) { create(:project_empty_repo, :public) } + + it 'redirects to the sign in page' do + request_csv + + expect(ExportCsvWorker).not_to receive(:perform_async) + expect(response).to redirect_to(new_user_session_path) + end + end + end + describe 'GET #discussions' do let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 5104c83283d..aaeaf53d100 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1245,7 +1245,7 @@ describe Projects::MergeRequestsController do end it 'renders MergeRequest as JSON' do - expect(json_response.keys).to include('id', 'iid') + expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'merge_status', 'can_be_merged', 'current_user') end end @@ -1279,7 +1279,7 @@ describe Projects::MergeRequestsController do it 'renders MergeRequest as JSON' do subject - expect(json_response.keys).to include('id', 'iid') + expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'merge_status', 'can_be_merged', 'current_user') end end diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index e565c757f95..59455d90c25 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -95,7 +95,7 @@ describe Repositories::GitHttpController do allow(controller).to receive(:access_check).and_return(nil) end - after do + def send_request post :git_upload_pack, params: params end @@ -106,16 +106,46 @@ describe Repositories::GitHttpController do it 'does not update project statistics' do expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async) + + send_request end end if expected - it 'updates project statistics' do - expect(ProjectDailyStatisticsWorker).to receive(:perform_async) + context 'when project_statistics_sync feature flag is disabled' do + before do + stub_feature_flags(project_statistics_sync: false) + end + + it 'updates project statistics async' do + expect(ProjectDailyStatisticsWorker).to receive(:perform_async) + + send_request + end + end + + it 'updates project statistics sync' do + expect { send_request }.to change { + Projects::DailyStatisticsFinder.new(project).total_fetch_count + }.from(0).to(1) end else + context 'when project_statistics_sync feature flag is disabled' do + before do + stub_feature_flags(project_statistics_sync: false) + end + + it 'does not update project statistics' do + expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async) + + send_request + end + end + it 'does not update project statistics' do - expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async) + expect { send_request }.not_to change { + Projects::DailyStatisticsFinder.new(project).total_fetch_count + }.from(0) end end end diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb index bacf163896c..4c1d5f07a42 100644 --- a/spec/factories/ci/bridge.rb +++ b/spec/factories/ci/bridge.rb @@ -7,7 +7,7 @@ FactoryBot.define do stage_idx { 0 } ref { 'master' } tag { false } - created_at { 'Di 29. Okt 09:50:00 CET 2013' } + created_at { '2013-10-29 09:50:00 CET' } status { :created } scheduling_type { 'stage' } @@ -39,5 +39,19 @@ FactoryBot.define do ) end end + + trait :started do + started_at { '2013-10-29 09:51:28 CET' } + end + + trait :finished do + started + finished_at { '2013-10-29 09:53:28 CET' } + end + + trait :failed do + finished + status { 'failed' } + end end end diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index a259c5142fc..82383cfa2b0 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -13,7 +13,7 @@ FactoryBot.define do end trait :remote_store do - file_store { JobArtifactUploader::Store::REMOTE } + file_store { JobArtifactUploader::Store::REMOTE} end after :build do |artifact| diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 0a4f0fba9ab..728c83e01b4 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -139,5 +139,14 @@ FactoryBot.define do cluster factory: %i(cluster provided_by_gcp) end end + + factory :clusters_applications_fluentd, class: 'Clusters::Applications::Fluentd' do + host { 'example.com' } + cluster factory: %i(cluster with_installed_helm provided_by_gcp) + + trait :no_helm_installed do + cluster factory: %i(cluster provided_by_gcp) + end + end end end diff --git a/spec/factories/diff_position.rb b/spec/factories/diff_position.rb index a43c5afdff4..685272acf5c 100644 --- a/spec/factories/diff_position.rb +++ b/spec/factories/diff_position.rb @@ -34,10 +34,20 @@ FactoryBot.define do position_type { 'text' } old_line { 10 } new_line { 10 } + line_range { nil } trait :added do old_line { nil } end + + trait :multi_line do + line_range do + { + start_line_code: Gitlab::Git.diff_line_code(file, 10, 10), + end_line_code: Gitlab::Git.diff_line_code(file, 12, 13) + } + end + end end factory :image_diff_position do diff --git a/spec/factories/import_failures.rb b/spec/factories/import_failures.rb new file mode 100644 index 00000000000..376b2ff39e2 --- /dev/null +++ b/spec/factories/import_failures.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'securerandom' + +FactoryBot.define do + factory :import_failure do + association :project, factory: :project + + created_at { Time.parse('2020-01-01T00:00:00Z') } + exception_class { 'RuntimeError' } + exception_message { 'Something went wrong' } + source { 'method_call' } + correlation_id_value { SecureRandom.uuid } + + trait :hard_failure do + retry_count { 0 } + end + + trait :soft_failure do + retry_count { 1 } + end + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 2b468ef92e1..64321c9f319 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -37,6 +37,8 @@ FactoryBot.define do group_runners_enabled { nil } import_status { nil } import_jid { nil } + import_correlation_id { nil } + import_last_error { nil } forward_deployment_enabled { nil } end @@ -78,6 +80,8 @@ FactoryBot.define do import_state = project.import_state || project.build_import_state import_state.status = evaluator.import_status import_state.jid = evaluator.import_jid + import_state.correlation_id_value = evaluator.import_correlation_id + import_state.last_error = evaluator.import_last_error import_state.save end end diff --git a/spec/factories/terraform/state.rb b/spec/factories/terraform/state.rb index 4b83128ff6e..74950ccf93e 100644 --- a/spec/factories/terraform/state.rb +++ b/spec/factories/terraform/state.rb @@ -4,8 +4,10 @@ FactoryBot.define do factory :terraform_state, class: 'Terraform::State' do project { create(:project) } + sequence(:name) { |n| "state-#{n}" } + trait :with_file do - file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate') } + file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') } end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 0ce567e11fe..f274503f0e7 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -27,6 +27,10 @@ FactoryBot.define do user_type { :alert_bot } end + trait :project_bot do + user_type { :project_bot } + end + trait :external do external { true } end @@ -83,12 +87,17 @@ FactoryBot.define do transient do developer_projects { [] } + maintainer_projects { [] } end after(:create) do |user, evaluator| evaluator.developer_projects.each do |project| project.add_developer(user) end + + evaluator.maintainer_projects.each do |project| + project.add_maintainer(user) + end end factory :omniauth_user do diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb index e33c9d7e64c..0ca61e6c193 100644 --- a/spec/features/admin/admin_mode/workers_spec.rb +++ b/spec/features/admin/admin_mode/workers_spec.rb @@ -8,8 +8,6 @@ describe 'Admin mode for workers', :do_not_mock_admin_mode, :request_store, :cle let(:user_to_delete) { create(:user) } before do - add_sidekiq_middleware - sign_in(user) end @@ -60,12 +58,6 @@ describe 'Admin mode for workers', :do_not_mock_admin_mode, :request_store, :cle end end - def add_sidekiq_middleware - Sidekiq::Testing.server_middleware do |chain| - chain.add Gitlab::SidekiqMiddleware::AdminMode::Server - end - end - def execute_jobs_signed_out(user) gitlab_sign_out diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 8eb15bb6bf5..1a3da8cb373 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -348,12 +348,19 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc it 'loads usage ping payload on click', :js do allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) - expect(page).to have_button 'Preview payload' + page.within('#js-usage-settings') do + expected_payload_content = /(?=.*"uuid")(?=.*"hostname")/m - find('.js-usage-ping-payload-trigger').click + expect(page).not_to have_content expected_payload_content - expect(page).to have_selector '.js-usage-ping-payload' - expect(page).to have_button 'Hide payload' + click_button('Preview payload') + + wait_for_requests + + expect(page).to have_selector '.js-usage-ping-payload' + expect(page).to have_button 'Hide payload' + expect(page).to have_content expected_payload_content + end end end diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 4a20d1b7d60..50d9cb1c833 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -30,6 +30,7 @@ describe 'Value Stream Analytics', :js do expect(new_issues_counter).to have_content('-') expect(commits_counter).to have_content('-') expect(deploys_counter).to have_content('-') + expect(deployment_frequency_counter).to have_content('-') end it 'shows active stage with empty message' do @@ -53,6 +54,7 @@ describe 'Value Stream Analytics', :js do expect(new_issues_counter).to have_content('1') expect(commits_counter).to have_content('2') expect(deploys_counter).to have_content('1') + expect(deployment_frequency_counter).to have_content('0') end it 'shows data on each stage', :sidekiq_might_not_need_inline do @@ -134,7 +136,15 @@ describe 'Value Stream Analytics', :js do end def deploys_counter - find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3") + find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3", match: :first) + end + + def deployment_frequency_counter_selector + "//p[contains(text(),'Deployment Frequency')]/preceding-sibling::h3" + end + + def deployment_frequency_counter + find(:xpath, deployment_frequency_counter_selector) end def expect_issue_to_be_present diff --git a/spec/features/issues/csv_spec.rb b/spec/features/issues/csv_spec.rb new file mode 100644 index 00000000000..193c83d2a40 --- /dev/null +++ b/spec/features/issues/csv_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Issues csv' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, title: 'v1.0', project: project) } + let(:idea_label) { create(:label, project: project, title: 'Idea') } + let(:feature_label) { create(:label, project: project, title: 'Feature', priority: 10) } + let!(:issue) { create(:issue, project: project, author: user) } + + before do + sign_in(user) + end + + def request_csv(params = {}) + visit project_issues_path(project, params) + page.within('.nav-controls') do + click_on 'Export as CSV' + end + click_on 'Export issues' + end + + def attachment + ActionMailer::Base.deliveries.last.attachments.first + end + + def csv + CSV.parse(attachment.decode_body, headers: true) + end + + it 'triggers an email export' do + expect(ExportCsvWorker).to receive(:perform_async).with(user.id, project.id, hash_including("project_id" => project.id)) + + request_csv + end + + it "doesn't send request params to ExportCsvWorker" do + expect(ExportCsvWorker).to receive(:perform_async).with(anything, anything, hash_excluding("controller" => anything, "action" => anything)) + + request_csv + end + + it 'displays flash message' do + request_csv + + expect(page).to have_content 'CSV export has started' + expect(page).to have_content "emailed to #{user.notification_email}" + end + + it 'includes a csv attachment', :sidekiq_might_not_need_inline do + request_csv + + expect(attachment.content_type).to include('text/csv') + end + + it 'ignores pagination', :sidekiq_might_not_need_inline do + create_list(:issue, 30, project: project, author: user) + + request_csv + + expect(csv.count).to eq 31 + end + + it 'uses filters from issue index', :sidekiq_might_not_need_inline do + request_csv(state: :closed) + + expect(csv.count).to eq 0 + end + + it 'ignores sorting from issue index', :sidekiq_might_not_need_inline do + issue2 = create(:labeled_issue, project: project, author: user, labels: [feature_label]) + + request_csv(sort: :label_priority) + + expected = [issue.iid.to_s, issue2.iid.to_s] + expect(csv.map { |row| row['Issue ID'] }).to eq expected + end + + it 'uses array filters, such as label_name', :sidekiq_might_not_need_inline do + issue.update!(labels: [idea_label]) + + request_csv("label_name[]" => 'Bug') + + expect(csv.count).to eq 0 + end + + it 'avoids excessive database calls' do + control_count = ActiveRecord::QueryRecorder.new { request_csv }.count + create_list(:labeled_issue, + 10, + project: project, + assignees: [user], + author: user, + milestone: milestone, + labels: [feature_label, idea_label]) + expect { request_csv }.not_to exceed_query_limit(control_count + 5) + end +end diff --git a/spec/features/merge_request/user_resolves_wip_mr_spec.rb b/spec/features/merge_request/user_resolves_wip_mr_spec.rb new file mode 100644 index 00000000000..93ef0801791 --- /dev/null +++ b/spec/features/merge_request/user_resolves_wip_mr_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Merge request > User resolves Work in Progress', :js do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:merge_request) do + create(:merge_request_with_diffs, source_project: project, + author: user, + title: 'WIP: Bug NS-04', + merge_params: { force_remove_source_branch: '1' }) + end + let(:pipeline) do + create(:ci_pipeline, project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + head_pipeline_of: merge_request) + end + + before do + project.add_maintainer(user) + end + + context 'when there is active pipeline for merge request' do + before do + create(:ci_build, pipeline: pipeline) + sign_in(user) + visit project_merge_request_path(project, merge_request) + wait_for_requests + end + + it 'retains merge request data after clicking Resolve WIP status' do + expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}") + expect(page).to have_content "This is a Work in Progress" + + click_button('Resolve WIP status') + + wait_for_requests + + # If we don't disable the wait here, the test will wait until the + # merge request widget refreshes, which masks missing elements + # that should already be present. + expect(page.find('.ci-widget-content', wait: 0)).to have_content("Pipeline ##{pipeline.id}") + expect(page).not_to have_content('This is a Work in Progress') + end + end +end diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb index 2b2327940a5..a51f121bf59 100644 --- a/spec/features/projects/environments_pod_logs_spec.rb +++ b/spec/features/projects/environments_pod_logs_spec.rb @@ -57,7 +57,9 @@ describe 'Environment > Pod Logs', :js do expect(item.text).to eq(pod_names[i]) end end - expect(page).to have_content("Dec 13 14:04:22.123Z | kube-pod | Log 1 Dec 13 14:04:23.123Z | kube-pod | Log 2 Dec 13 14:04:24.123Z | kube-pod | Log 3") + expect(page).to have_content("kube-pod | Log 1") + expect(page).to have_content("kube-pod | Log 2") + expect(page).to have_content("kube-pod | Log 3") end end end diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb index b55a42e07a9..d883a1fc39c 100644 --- a/spec/features/projects/snippets/create_snippet_spec.rb +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' shared_examples_for 'snippet editor' do before do + stub_feature_flags(snippets_edit_vue: false) stub_feature_flags(monaco_snippets: flag) end diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb index bad3fde8a4a..cf501e55e23 100644 --- a/spec/features/projects/snippets/user_updates_snippet_spec.rb +++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb @@ -11,6 +11,7 @@ describe 'Projects > Snippets > User updates a snippet', :js do before do stub_feature_flags(snippets_vue: false) + stub_feature_flags(snippets_edit_vue: false) stub_feature_flags(version_snippets: version_snippet_enabled) project.add_maintainer(user) diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb index e9534dedcd3..69e3f190725 100644 --- a/spec/features/snippets/spam_snippets_spec.rb +++ b/spec/features/snippets/spam_snippets_spec.rb @@ -10,6 +10,7 @@ shared_examples_for 'snippet editor' do before do stub_feature_flags(allow_possible_spam: false) stub_feature_flags(snippets_vue: false) + stub_feature_flags(snippets_edit_vue: false) stub_feature_flags(monaco_snippets: flag) stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index 93da976dee0..5d3a84dd7bc 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' shared_examples_for 'snippet editor' do before do stub_feature_flags(snippets_vue: false) + stub_feature_flags(snippets_edit_vue: false) stub_feature_flags(monaco_snippets: flag) sign_in(user) visit new_snippet_path diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb index 0bbb92b1f3f..b4f8fbfa47e 100644 --- a/spec/features/snippets/user_edits_snippet_spec.rb +++ b/spec/features/snippets/user_edits_snippet_spec.rb @@ -14,6 +14,7 @@ describe 'User edits snippet', :js do before do stub_feature_flags(snippets_vue: false) + stub_feature_flags(snippets_edit_vue: false) stub_feature_flags(version_snippets: version_snippet_enabled) sign_in(user) diff --git a/spec/features/static_site_editor_spec.rb b/spec/features/static_site_editor_spec.rb new file mode 100644 index 00000000000..c457002f888 --- /dev/null +++ b/spec/features/static_site_editor_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Static Site Editor' do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + + before do + project.add_maintainer(user) + sign_in(user) + + visit project_show_sse_path(project, 'master/README.md') + end + + it 'renders Static Site Editor page' do + expect(page).to have_selector('#static-site-editor') + end +end diff --git a/spec/finders/autocomplete/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb index 9129a3b65be..f2da82bb9be 100644 --- a/spec/finders/autocomplete/move_to_project_finder_spec.rb +++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb @@ -62,19 +62,20 @@ describe Autocomplete::MoveToProjectFinder do expect(finder.execute.to_a).to eq([other_reporter_project]) end - it 'returns a page of projects ordered by name' do + it 'returns a page of projects ordered by star count' do stub_const('Autocomplete::MoveToProjectFinder::LIMIT', 2) - projects = create_list(:project, 3) do |project| - project.add_developer(user) - end + projects = [ + create(:project, namespace: user.namespace, star_count: 1), + create(:project, namespace: user.namespace, star_count: 5), + create(:project, namespace: user.namespace) + ] finder = described_class.new(user, project_id: project.id) page = finder.execute.to_a - expected_projects = projects.sort_by(&:name).first(2) expect(page.length).to eq(2) - expect(page).to eq(expected_projects) + expect(page).to eq([projects[1], projects[0]]) end end diff --git a/spec/finders/metrics/dashboards/annotations_finder_spec.rb b/spec/finders/metrics/dashboards/annotations_finder_spec.rb new file mode 100644 index 00000000000..222875ba2e2 --- /dev/null +++ b/spec/finders/metrics/dashboards/annotations_finder_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Metrics::Dashboards::AnnotationsFinder do + describe '#execute' do + subject(:annotations) { described_class.new(dashboard: dashboard, params: params).execute } + + let_it_be(:current_user) { create(:user) } + let(:path) { 'config/prometheus/common_metrics.yml' } + let(:params) { {} } + let(:environment) { create(:environment) } + let(:dashboard) { PerformanceMonitoring::PrometheusDashboard.new(path: path, environment: environment) } + + context 'there are no annotations records' do + it 'returns empty array' do + expect(annotations).to be_empty + end + end + + context 'with annotation records' do + let!(:nine_minutes_old_annotation) { create(:metrics_dashboard_annotation, environment: environment, starting_at: 9.minutes.ago, dashboard_path: path) } + let!(:fifteen_minutes_old_annotation) { create(:metrics_dashboard_annotation, environment: environment, starting_at: 15.minutes.ago, dashboard_path: path) } + let!(:just_created_annotation) { create(:metrics_dashboard_annotation, environment: environment, dashboard_path: path) } + let!(:annotation_for_different_env) { create(:metrics_dashboard_annotation, dashboard_path: path) } + let!(:annotation_for_different_dashboard) { create(:metrics_dashboard_annotation, dashboard_path: '.gitlab/dashboards/test.yml') } + + it 'loads annotations' do + expect(annotations).to match_array [fifteen_minutes_old_annotation, nine_minutes_old_annotation, just_created_annotation] + end + + context 'when the from filter is present' do + let(:params) do + { + from: 14.minutes.ago + } + end + + it 'loads only younger annotations' do + expect(annotations).to match_array [nine_minutes_old_annotation, just_created_annotation] + end + end + + context 'when the to filter is present' do + let(:params) do + { + to: 5.minutes.ago + } + end + + it 'loads only older annotations' do + expect(annotations).to match_array [fifteen_minutes_old_annotation, nine_minutes_old_annotation] + end + end + + context 'when from and to filters are present' do + context 'and to is bigger than from' do + let(:params) do + { + from: 14.minutes.ago, + to: 5.minutes.ago + } + end + + it 'loads only annotations assigned to this interval' do + expect(annotations).to match_array [nine_minutes_old_annotation] + end + end + + context 'and from is bigger than to' do + let(:params) do + { + to: 14.minutes.ago, + from: 5.minutes.ago + } + end + + it 'ignores to parameter and returns annotations starting at from filter' do + expect(annotations).to match_array [just_created_annotation] + end + end + + context 'when from or to filters are empty strings' do + let(:params) do + { + from: '', + to: '' + } + end + + it 'ignores this parameters' do + expect(annotations).to match_array [fifteen_minutes_old_annotation, nine_minutes_old_annotation, just_created_annotation] + end + end + end + + context 'dashboard environment is missing' do + let(:dashboard) { PerformanceMonitoring::PrometheusDashboard.new(path: path, environment: nil) } + + it 'returns empty relation', :aggregate_failures do + expect(annotations).to be_kind_of ::ActiveRecord::Relation + expect(annotations).to be_empty + end + end + end + end +end diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index ba97b7c82cb..ce62655648b 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -39,6 +39,9 @@ "stack": { "type": ["string", "null"] }, "modsecurity_enabled": { "type": ["boolean", "null"] }, "modsecurity_mode": {"type": ["integer", "0"]}, + "host": {"type": ["string", "null"]}, + "port": {"type": ["integer", "514"]}, + "protocol": {"type": ["integer", "0"]}, "update_available": { "type": ["boolean", "null"] }, "can_uninstall": { "type": "boolean" }, "available_domains": { diff --git a/spec/fixtures/lib/elasticsearch/pods_query.json b/spec/fixtures/lib/elasticsearch/pods_query.json new file mode 100644 index 00000000000..90d162b871a --- /dev/null +++ b/spec/fixtures/lib/elasticsearch/pods_query.json @@ -0,0 +1,28 @@ +{ + "aggs": { + "pods": { + "aggs": { + "containers": { + "terms": { + "field": "kubernetes.container.name", + "size": 500 + } + } + }, + "terms": { + "field": "kubernetes.pod.name", + "size": 500 + } + } + }, + "query": { + "bool": { + "must": { + "match_phrase": { + "kubernetes.namespace": "autodevops-deploy-9-production" + } + } + } + }, + "size": 0 +} diff --git a/spec/fixtures/lib/elasticsearch/pods_response.json b/spec/fixtures/lib/elasticsearch/pods_response.json new file mode 100644 index 00000000000..d923f914d7c --- /dev/null +++ b/spec/fixtures/lib/elasticsearch/pods_response.json @@ -0,0 +1,75 @@ +{ + "took": 8540, + "timed_out": false, + "_shards": { + "total": 153, + "successful": 153, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": 62143, + "max_score": 0.0, + "hits": [ + + ] + }, + "aggregations": { + "pods": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "runner-gitlab-runner-7bbfb5dcb5-p6smb", + "doc_count": 19795, + "containers": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "runner-gitlab-runner", + "doc_count": 19795 + } + ] + } + }, + { + "key": "elastic-stack-elasticsearch-master-1", + "doc_count": 13185, + "containers": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "elasticsearch", + "doc_count": 13158 + }, + { + "key": "chown", + "doc_count": 24 + }, + { + "key": "sysctl", + "doc_count": 3 + } + ] + } + }, + { + "key": "ingress-nginx-ingress-controller-76449bcc8d-8qgl6", + "doc_count": 3437, + "containers": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "nginx-ingress-controller", + "doc_count": 3437 + } + ] + } + } + ] + } + } +} diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js index 189d2629efa..971ef72521d 100644 --- a/spec/frontend/blob/components/blob_edit_content_spec.js +++ b/spec/frontend/blob/components/blob_edit_content_spec.js @@ -80,7 +80,7 @@ describe('Blob Header Editing', () => { getValue: jest.fn().mockReturnValue(value), }; - editorEl.trigger('focusout'); + editorEl.trigger('keyup'); return nextTick().then(() => { expect(wrapper.emitted().input[0]).toEqual([value]); diff --git a/spec/frontend/ci_variable_list/components/ci_key_field_spec.js b/spec/frontend/ci_variable_list/components/ci_key_field_spec.js new file mode 100644 index 00000000000..bcc29f22dd1 --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_key_field_spec.js @@ -0,0 +1,244 @@ +import { mount } from '@vue/test-utils'; +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION } from '~/ci_variable_list/constants'; +import CiKeyField from '~/ci_variable_list/components/ci_key_field.vue'; + +import { + awsTokens, + awsTokenList, +} from '~/ci_variable_list/components/ci_variable_autocomplete_tokens'; + +const doTimes = (num, fn) => { + for (let i = 0; i < num; i += 1) { + fn(); + } +}; + +describe('Ci Key field', () => { + let wrapper; + + const createComponent = () => { + wrapper = mount({ + data() { + return { + inputVal: '', + tokens: awsTokenList, + }; + }, + components: { CiKeyField }, + template: ` +
+ +
+ `, + }); + }; + + const findDropdown = () => wrapper.find('#ci-variable-dropdown'); + const findDropdownOptions = () => wrapper.findAll(GlButton).wrappers.map(item => item.text()); + const findInput = () => wrapper.find(GlFormInput); + const findInputValue = () => findInput().element.value; + const setInput = val => findInput().setValue(val); + const clickDown = () => findInput().trigger('keydown.down'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('match and filter functionality', () => { + beforeEach(() => { + createComponent(); + }); + + it('is closed when the input is empty', () => { + expect(findInput().isVisible()).toBe(true); + expect(findInputValue()).toBe(''); + expect(findDropdown().isVisible()).toBe(false); + }); + + it('is open when the input text matches a token', () => { + setInput('AWS'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(true); + }); + }); + + it('shows partial matches at string start', () => { + setInput('AWS'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(true); + expect(findDropdownOptions()).toEqual(awsTokenList); + }); + }); + + it('shows partial matches mid-string', () => { + setInput('D'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(true); + expect(findDropdownOptions()).toEqual([ + awsTokens[AWS_ACCESS_KEY_ID].name, + awsTokens[AWS_DEFAULT_REGION].name, + ]); + }); + }); + + it('is closed when the text does not match', () => { + setInput('elephant'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(false); + }); + }); + }); + + describe('keyboard navigation in dropdown', () => { + beforeEach(() => { + createComponent(); + }); + + describe('on down arrow + enter', () => { + it('selects the next item in the list and closes the dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.down'); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[0]); + }); + }); + + it('loops to the top when it reaches the bottom', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + doTimes(findDropdownOptions().length + 1, clickDown); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[0]); + }); + }); + }); + + describe('on up arrow + enter', () => { + it('selects the previous item in the list and closes the dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + doTimes(3, clickDown); + findInput().trigger('keydown.up'); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[1]); + }); + }); + + it('loops to the bottom when it reaches the top', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.down'); + findInput().trigger('keydown.up'); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[awsTokenList.length - 1]); + }); + }); + }); + + describe('on enter with no item highlighted', () => { + it('does not select any item and closes the dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe('AWS'); + }); + }); + }); + + describe('on click', () => { + it('selects the clicked item regardless of arrow highlight', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + wrapper.find(GlButton).trigger('click'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[0]); + }); + }); + }); + + describe('on tab', () => { + it('selects entered text, closes dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.tab'); + doTimes(2, clickDown); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe('AWS'); + expect(findDropdown().isVisible()).toBe(false); + }); + }); + }); + + describe('on esc', () => { + describe('when dropdown is open', () => { + it('closes dropdown and does not select anything', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.esc'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe('AWS'); + expect(findDropdown().isVisible()).toBe(false); + }); + }); + }); + + describe('when dropdown is closed', () => { + it('clears the input field', () => { + setInput('elephant'); + return wrapper.vm + .$nextTick() + .then(() => { + expect(findDropdown().isVisible()).toBe(false); + findInput().trigger('keydown.esc'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(''); + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js index 70edd36669b..7b8d69df35e 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -1,7 +1,10 @@ import Vuex from 'vuex'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; import { GlDeprecatedButton } from '@gitlab/ui'; +import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants'; import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; +import CiKeyField from '~/ci_variable_list/components/ci_key_field.vue'; +import { awsTokens } from '~/ci_variable_list/components/ci_variable_autocomplete_tokens'; import createStore from '~/ci_variable_list/store'; import mockData from '../services/mock_data'; import ModalStub from '../stubs'; @@ -13,14 +16,17 @@ describe('Ci variable modal', () => { let wrapper; let store; - const createComponent = () => { + const createComponent = (method, options = {}) => { store = createStore(); - wrapper = shallowMount(CiVariableModal, { + wrapper = method(CiVariableModal, { + attachToDocument: true, + provide: { glFeatures: { ciKeyAutocomplete: true } }, stubs: { GlModal: ModalStub, }, localVue, store, + ...options, }); }; @@ -34,22 +40,46 @@ describe('Ci variable modal', () => { .findAll(GlDeprecatedButton) .at(1); - beforeEach(() => { - createComponent(); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - afterEach(() => { wrapper.destroy(); }); - it('button is disabled when no key/value pair are present', () => { - expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + describe('Feature flag', () => { + describe('when off', () => { + beforeEach(() => { + createComponent(shallowMount, { provide: { glFeatures: { ciKeyAutocomplete: false } } }); + }); + + it('does not render the autocomplete dropdown', () => { + expect(wrapper.contains(CiKeyField)).toBe(false); + }); + }); + + describe('when on', () => { + beforeEach(() => { + createComponent(shallowMount); + }); + it('renders the autocomplete dropdown', () => { + expect(wrapper.find(CiKeyField).exists()).toBe(true); + }); + }); + }); + + describe('Basic interactions', () => { + beforeEach(() => { + createComponent(shallowMount); + }); + + it('button is disabled when no key/value pair are present', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); }); describe('Adding a new variable', () => { beforeEach(() => { const [variable] = mockData.mockVariables; + createComponent(shallowMount); + jest.spyOn(store, 'dispatch').mockImplementation(); store.state.variable = variable; }); @@ -71,6 +101,8 @@ describe('Ci variable modal', () => { describe('Editing a variable', () => { beforeEach(() => { const [variable] = mockData.mockVariables; + createComponent(shallowMount); + jest.spyOn(store, 'dispatch').mockImplementation(); store.state.variableBeingEdited = variable; }); @@ -96,4 +128,105 @@ describe('Ci variable modal', () => { expect(store.dispatch).toHaveBeenCalledWith('deleteVariable', mockData.mockVariables[0]); }); }); + + describe('Validations', () => { + const maskError = 'This variable can not be masked.'; + + describe('when the key state is invalid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const invalidKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhy', + secret_value: 'AKIAIOSFODNN7EXAMPLEjdhy', + }; + createComponent(mount); + store.state.variable = invalidKeyVariable; + }); + + it('disables the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage; + expect(findModal().text()).toContain(errorText); + }); + }); + + describe('when the mask state is invalid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const invalidMaskVariable = { + ...variable, + key: 'qs', + value: 'd:;', + secret_value: 'd:;', + masked: true, + }; + createComponent(mount); + store.state.variable = invalidMaskVariable; + }); + + it('disables the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + expect(findModal().text()).toContain(maskError); + }); + }); + + describe('when the mask and key states are invalid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const invalidMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhyd:;', + secret_value: 'AKIAIOSFODNN7EXAMPLEjdhyd:;', + masked: true, + }; + createComponent(mount); + store.state.variable = invalidMaskandKeyVariable; + }); + + it('disables the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage; + expect(findModal().text()).toContain(maskError); + expect(findModal().text()).toContain(errorText); + }); + }); + + describe('when both states are valid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const validMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLE', + secret_value: 'AKIAIOSFODNN7EXAMPLE', + masked: true, + }; + createComponent(mount); + store.state.variable = validMaskandKeyVariable; + store.state.maskableRegex = /^[a-zA-Z0-9_+=/@:-]{8,}$/; + }); + + it('does not disable the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy(); + }); + + it('shows no error text', () => { + const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage; + expect(findModal().text()).not.toContain(maskError); + expect(findModal().text()).not.toContain(errorText); + }); + }); + }); }); diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js index 8632c5c4e26..b27cd2c80fd 100644 --- a/spec/frontend/clusters/services/application_state_machine_spec.js +++ b/spec/frontend/clusters/services/application_state_machine_spec.js @@ -161,4 +161,20 @@ describe('applicationStateMachine', () => { }); }); }); + + describe('current state is undefined', () => { + it('returns the current state without having any effects', () => { + const currentAppState = {}; + expect(transitionApplicationState(currentAppState, INSTALLABLE)).toEqual(currentAppState); + }); + }); + + describe('with event is undefined', () => { + it('returns the current state without having any effects', () => { + const currentAppState = { + status: NO_STATUS, + }; + expect(transitionApplicationState(currentAppState, undefined)).toEqual(currentAppState); + }); + }); }); diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js index 517d050eb54..6bb3a0dcf21 100644 --- a/spec/frontend/diffs/components/commit_item_spec.js +++ b/spec/frontend/diffs/components/commit_item_spec.js @@ -59,9 +59,7 @@ describe('diffs/components/commit_item', () => { expect(titleElement.text()).toBe(commit.title_html); }); - // https://gitlab.com/gitlab-org/gitlab/-/issues/209776 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('renders commit description', () => { + it('renders commit description', () => { const descElement = getDescElement(); const descExpandElement = getDescExpandElement(); diff --git a/spec/frontend/diffs/components/diff_table_cell_spec.js b/spec/frontend/diffs/components/diff_table_cell_spec.js index 1af0746f3bd..e871d86d901 100644 --- a/spec/frontend/diffs/components/diff_table_cell_spec.js +++ b/spec/frontend/diffs/components/diff_table_cell_spec.js @@ -85,15 +85,18 @@ describe('DiffTableCell', () => { describe('comment button', () => { it.each` - showCommentButton | userData | query | expectation - ${true} | ${TEST_USER} | ${'diff_head=false'} | ${true} - ${true} | ${TEST_USER} | ${'diff_head=true'} | ${false} - ${false} | ${TEST_USER} | ${'bogus'} | ${false} - ${true} | ${null} | ${''} | ${false} + showCommentButton | userData | query | mergeRefHeadComments | expectation + ${true} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true} + ${true} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true} + ${true} | ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false} + ${false} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${false} + ${false} | ${TEST_USER} | ${'bogus'} | ${true} | ${false} + ${true} | ${null} | ${''} | ${true} | ${false} `( 'exists is $expectation - with showCommentButton ($showCommentButton) userData ($userData) query ($query)', - ({ showCommentButton, userData, query, expectation }) => { + ({ showCommentButton, userData, query, mergeRefHeadComments, expectation }) => { store.state.notes.userData = userData; + gon.features = { mergeRefHeadComments }; setWindowLocation({ href: `${TEST_HOST}?${query}` }); createComponent({ showCommentButton }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 8a1c3e56e5a..ceccce6312f 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -466,6 +466,7 @@ describe('DiffsStoreActions', () => { old_path: 'file2', line_code: 'ABC_1_1', position_type: 'text', + line_range: null, }, }, hash: 'ABC_123', diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js index 3e5ba66d5e4..0343ef75732 100644 --- a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js +++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js @@ -1,6 +1,9 @@ import * as getters from '~/diffs/store/getters'; import state from '~/diffs/store/modules/diff_state'; -import { DIFF_COMPARE_BASE_VERSION_INDEX } from '~/diffs/constants'; +import { + DIFF_COMPARE_BASE_VERSION_INDEX, + DIFF_COMPARE_HEAD_VERSION_INDEX, +} from '~/diffs/constants'; import diffsMockData from '../mock_data/merge_request_diffs'; describe('Compare diff version dropdowns', () => { @@ -37,47 +40,93 @@ describe('Compare diff version dropdowns', () => { describe('diffCompareDropdownTargetVersions', () => { // diffCompareDropdownTargetVersions slices the array at the first position - // and appends a "base" version which is why we use diffsMockData[1] below - // This is to display "base" at the end of the target dropdown - const expectedFirstVersion = { - ...diffsMockData[1], - href: expect.any(String), - versionName: expect.any(String), + // and appends a "base" and "head" version at the end of the list so that + // "base" and "head" appear at the bottom of the dropdown + // this is also why we use diffsMockData[1] for the "first" version + + let expectedFirstVersion; + let expectedBaseVersion; + let expectedHeadVersion; + const originalLocation = window.location; + + const setupTest = includeDiffHeadParam => { + const diffHeadParam = includeDiffHeadParam ? '?diff_head=true' : ''; + + Object.defineProperty(window, 'location', { + writable: true, + value: { href: `https://example.gitlab.com${diffHeadParam}` }, + }); + + expectedFirstVersion = { + ...diffsMockData[1], + href: expect.any(String), + versionName: expect.any(String), + selected: false, + }; + + expectedBaseVersion = { + versionName: 'baseVersion', + version_index: DIFF_COMPARE_BASE_VERSION_INDEX, + href: 'basePath', + isBase: true, + selected: false, + }; + + expectedHeadVersion = { + versionName: 'baseVersion', + version_index: DIFF_COMPARE_HEAD_VERSION_INDEX, + href: 'headPath', + isHead: true, + selected: false, + }; }; - const expectedBaseVersion = { - versionName: 'baseVersion', - version_index: DIFF_COMPARE_BASE_VERSION_INDEX, - href: 'basePath', - isBase: true, + const assertVersions = targetVersions => { + // base and head should be the last two versions in that order + const targetBaseVersion = targetVersions[targetVersions.length - 2]; + const targetHeadVersion = targetVersions[targetVersions.length - 1]; + expect(targetVersions[0]).toEqual(expectedFirstVersion); + expect(targetBaseVersion).toEqual(expectedBaseVersion); + expect(targetHeadVersion).toEqual(expectedHeadVersion); }; + afterEach(() => { + window.location = originalLocation; + }); + it('base version selected', () => { - expectedFirstVersion.selected = false; + setupTest(); expectedBaseVersion.selected = true; - const targetVersions = getters.diffCompareDropdownTargetVersions(localState, { - selectedTargetIndex: DIFF_COMPARE_BASE_VERSION_INDEX, - }); + const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters); + assertVersions(targetVersions); + }); - const lastVersion = targetVersions[targetVersions.length - 1]; - expect(targetVersions[0]).toEqual(expectedFirstVersion); - expect(lastVersion).toEqual(expectedBaseVersion); + it('head version selected', () => { + setupTest(true); + + expectedHeadVersion.selected = true; + + const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters); + assertVersions(targetVersions); }); it('first version selected', () => { - expectedFirstVersion.selected = true; - expectedBaseVersion.selected = false; + // NOTE: It should not be possible to have both "diff_head=true" and + // have anything other than the head version selected, but the user could + // manually add "?diff_head=true" to the url. In this instance we still + // want the actual selected version to display as "selected" + // Passing in "true" here asserts that first version is still selected + // even if "diff_head" is present in the url + setupTest(true); + expectedFirstVersion.selected = true; localState.startVersion = expectedFirstVersion; const targetVersions = getters.diffCompareDropdownTargetVersions(localState, { selectedTargetIndex: expectedFirstVersion.version_index, }); - - const lastVersion = targetVersions[targetVersions.length - 1]; - expect(targetVersions[0]).toEqual(expectedFirstVersion); - expect(lastVersion).toEqual(expectedBaseVersion); + assertVersions(targetVersions); }); }); diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index c44feaf4b63..858ab5be167 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -615,6 +615,73 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); }); + + it('should add discussions by line_codes and positions attributes', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_2', + line_codes: ['ABC_1'], + diff_discussion: true, + resolvable: true, + original_position: {}, + position: {}, + positions: [diffPosition], + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions).toHaveLength(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toBe(1); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions).toHaveLength(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toBe(1); + }); }); describe('REMOVE_LINE_DISCUSSIONS', () => { diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb index 7997ee79a01..76bb8567a64 100644 --- a/spec/frontend/fixtures/merge_requests_diffs.rb +++ b/spec/frontend/fixtures/merge_requests_diffs.rb @@ -10,7 +10,6 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } let(:path) { "files/ruby/popen.rb" } - let(:selected_commit) { merge_request.all_commits[0] } let(:position) do build(:text_diff_position, :added, file: path, @@ -34,11 +33,11 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type end it 'merge_request_diffs/with_commit.json' do - # Create a user that matches the selected commit author + # Create a user that matches the project.commit author # This is so that the "author" information will be populated - create(:user, email: selected_commit.author_email, name: selected_commit.author_name) + create(:user, email: project.commit.author_email, name: project.commit.author_name) - render_merge_request(merge_request, commit_id: selected_commit.sha) + render_merge_request(merge_request, commit_id: project.commit.sha) end it 'merge_request_diffs/inline_changes_tab_with_comments.json' do diff --git a/spec/frontend/helpers/dom_events_helper.js b/spec/frontend/helpers/dom_events_helper.js new file mode 100644 index 00000000000..b66c12daf4f --- /dev/null +++ b/spec/frontend/helpers/dom_events_helper.js @@ -0,0 +1,10 @@ +export const triggerDOMEvent = type => { + window.document.dispatchEvent( + new Event(type, { + bubbles: true, + cancelable: true, + }), + ); +}; + +export default () => {}; diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js index fb3ffe1ede3..ce32559d5c9 100644 --- a/spec/frontend/jira_import/components/jira_import_app_spec.js +++ b/spec/frontend/jira_import/components/jira_import_app_spec.js @@ -1,38 +1,213 @@ +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import JiraImportApp from '~/jira_import/components/jira_import_app.vue'; +import JiraImportForm from '~/jira_import/components/jira_import_form.vue'; +import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; +import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql'; +import { IMPORT_STATE } from '~/jira_import/utils'; + +const mountComponent = ({ + isJiraConfigured = true, + errorMessage = '', + showAlert = true, + status = IMPORT_STATE.NONE, + loading = false, + mutate = jest.fn(() => Promise.resolve()), +} = {}) => + shallowMount(JiraImportApp, { + propsData: { + isJiraConfigured, + inProgressIllustration: 'in-progress-illustration.svg', + issuesPath: 'gitlab-org/gitlab-test/-/issues', + jiraProjects: [ + ['My Jira Project', 'MJP'], + ['My Second Jira Project', 'MSJP'], + ['Migrate to GitLab', 'MTG'], + ], + projectPath: 'gitlab-org/gitlab-test', + setupIllustration: 'setup-illustration.svg', + }, + data() { + return { + errorMessage, + showAlert, + jiraImportDetails: { + status, + import: { + jiraProjectKey: 'MTG', + scheduledAt: '2020-04-08T12:17:25+00:00', + scheduledBy: { + name: 'Jane Doe', + }, + }, + }, + }; + }, + mocks: { + $apollo: { + loading, + mutate, + }, + }, + }); describe('JiraImportApp', () => { let wrapper; + const getFormComponent = () => wrapper.find(JiraImportForm); + + const getProgressComponent = () => wrapper.find(JiraImportProgress); + + const getSetupComponent = () => wrapper.find(JiraImportSetup); + + const getAlert = () => wrapper.find(GlAlert); + + const getLoadingIcon = () => wrapper.find(GlLoadingIcon); + afterEach(() => { wrapper.destroy(); wrapper = null; }); - describe('set up Jira integration page', () => { + describe('when Jira integration is not configured', () => { + beforeEach(() => { + wrapper = mountComponent({ isJiraConfigured: false }); + }); + + it('shows the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(true); + }); + + it('does not show loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('does not show the "Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(false); + }); + + it('does not show the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(false); + }); + }); + + describe('when Jira integration is configured but data is being fetched', () => { + beforeEach(() => { + wrapper = mountComponent({ loading: true }); + }); + + it('does not show the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(false); + }); + + it('shows loading icon', () => { + expect(getLoadingIcon().exists()).toBe(true); + }); + + it('does not show the "Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(false); + }); + + it('does not show the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(false); + }); + }); + + describe('when Jira integration is configured but import is in progress', () => { + beforeEach(() => { + wrapper = mountComponent({ status: IMPORT_STATE.SCHEDULED }); + }); + + it('does not show the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(false); + }); + + it('does not show loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('shows the "Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(true); + }); + + it('does not show the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(false); + }); + }); + + describe('when Jira integration is configured and there is no import in progress', () => { beforeEach(() => { - wrapper = shallowMount(JiraImportApp, { - propsData: { - isJiraConfigured: true, - projectPath: 'gitlab-org/gitlab-test', - setupIllustration: 'illustration.svg', + wrapper = mountComponent(); + }); + + it('does not show the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(false); + }); + + it('does not show loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('does not show the Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(false); + }); + + it('shows the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(true); + }); + }); + + describe('initiating a Jira import', () => { + it('calls the mutation with the expected arguments', () => { + const mutate = jest.fn(() => Promise.resolve()); + + wrapper = mountComponent({ mutate }); + + const mutationArguments = { + mutation: initiateJiraImportMutation, + variables: { + input: { + jiraProjectKey: 'MTG', + projectPath: 'gitlab-org/gitlab-test', + }, }, - }); + }; + + getFormComponent().vm.$emit('initiateJiraImport', 'MTG'); + + expect(mutate).toHaveBeenCalledWith(expect.objectContaining(mutationArguments)); }); - it('is shown when Jira integration is not configured', () => { - wrapper.setProps({ - isJiraConfigured: false, - }); + it('shows alert message with error message on error', () => { + const mutate = jest.fn(() => Promise.reject()); + + wrapper = mountComponent({ mutate }); + + getFormComponent().vm.$emit('initiateJiraImport', 'MTG'); + + // One tick doesn't update the dom to the desired state so we have two ticks here + return Vue.nextTick() + .then(Vue.nextTick) + .then(() => { + expect(getAlert().text()).toBe('There was an error importing the Jira project.'); + }); + }); + }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.find(JiraImportSetup).exists()).toBe(true); - }); + it('can dismiss alert message', () => { + wrapper = mountComponent({ + errorMessage: 'There was an error importing the Jira project.', + showAlert: true, }); - it('is not shown when Jira integration is configured', () => { - expect(wrapper.find(JiraImportSetup).exists()).toBe(false); + expect(getAlert().exists()).toBe(true); + + getAlert().vm.$emit('dismiss'); + + return Vue.nextTick().then(() => { + expect(getAlert().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js index 315ccccd991..0987eb11693 100644 --- a/spec/frontend/jira_import/components/jira_import_form_spec.js +++ b/spec/frontend/jira_import/components/jira_import_form_spec.js @@ -1,62 +1,126 @@ -import { GlAvatar, GlNewButton, GlFormSelect, GlLabel } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlAvatar, GlButton, GlFormSelect, GlLabel } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; import JiraImportForm from '~/jira_import/components/jira_import_form.vue'; +const mountComponent = ({ mountType } = {}) => { + const mountFunction = mountType === 'mount' ? mount : shallowMount; + + return mountFunction(JiraImportForm, { + propsData: { + issuesPath: 'gitlab-org/gitlab-test/-/issues', + jiraProjects: [ + { + text: 'My Jira Project', + value: 'MJP', + }, + { + text: 'My Second Jira Project', + value: 'MSJP', + }, + { + text: 'Migrate to GitLab', + value: 'MTG', + }, + ], + }, + }); +}; + describe('JiraImportForm', () => { let wrapper; - beforeEach(() => { - wrapper = shallowMount(JiraImportForm); - }); + const getCancelButton = () => wrapper.findAll(GlButton).at(1); afterEach(() => { wrapper.destroy(); wrapper = null; }); - it('shows a dropdown to choose the Jira project to import from', () => { - expect(wrapper.find(GlFormSelect).exists()).toBe(true); - }); + describe('select dropdown', () => { + it('is shown', () => { + wrapper = mountComponent(); - it('shows a label which will be applied to imported Jira projects', () => { - expect(wrapper.find(GlLabel).attributes('title')).toBe('jira-import::KEY-1'); - }); + expect(wrapper.find(GlFormSelect).exists()).toBe(true); + }); - it('shows information to the user', () => { - expect(wrapper.find('p').text()).toBe( - "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", - ); - }); + it('contains a list of Jira projects to select from', () => { + wrapper = mountComponent({ mountType: 'mount' }); - it('shows jira.issue.summary for the Title', () => { - expect(wrapper.find('[id="jira-project-title"]').text()).toBe('jira.issue.summary'); + const optionItems = ['My Jira Project', 'My Second Jira Project', 'Migrate to GitLab']; + + wrapper + .find(GlFormSelect) + .findAll('option') + .wrappers.forEach((optionEl, index) => { + expect(optionEl.text()).toBe(optionItems[index]); + }); + }); }); - it('shows an avatar for the Reporter', () => { - expect(wrapper.find(GlAvatar).exists()).toBe(true); + describe('form information', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('shows a label which will be applied to imported Jira projects', () => { + expect(wrapper.find(GlLabel).attributes('title')).toBe('jira-import::KEY-1'); + }); + + it('shows information to the user', () => { + expect(wrapper.find('p').text()).toBe( + "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", + ); + }); + + it('shows jira.issue.summary for the Title', () => { + expect(wrapper.find('[id="jira-project-title"]').text()).toBe('jira.issue.summary'); + }); + + it('shows an avatar for the Reporter', () => { + expect(wrapper.find(GlAvatar).exists()).toBe(true); + }); + + it('shows jira.issue.description.content for the Description', () => { + expect(wrapper.find('[id="jira-project-description"]').text()).toBe( + 'jira.issue.description.content', + ); + }); }); - it('shows jira.issue.description.content for the Description', () => { - expect(wrapper.find('[id="jira-project-description"]').text()).toBe( - 'jira.issue.description.content', - ); + describe('Next button', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('is shown', () => { + expect(wrapper.find(GlButton).text()).toBe('Next'); + }); }); - it('shows a Next button', () => { - const nextButton = wrapper - .findAll(GlNewButton) - .at(0) - .text(); + describe('Cancel button', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('is shown', () => { + expect(getCancelButton().text()).toBe('Cancel'); + }); - expect(nextButton).toBe('Next'); + it('links to the Issues page', () => { + expect(getCancelButton().attributes('href')).toBe('gitlab-org/gitlab-test/-/issues'); + }); }); - it('shows a Cancel button', () => { - const cancelButton = wrapper - .findAll(GlNewButton) - .at(1) - .text(); + it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => { + const selectedOption = 'MTG'; + + wrapper = mountComponent(); + wrapper.setData({ + selectedOption, + }); + + wrapper.find('form').trigger('submit'); - expect(cancelButton).toBe('Cancel'); + expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([selectedOption]); }); }); diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js new file mode 100644 index 00000000000..9a6fc3b5925 --- /dev/null +++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js @@ -0,0 +1,70 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; + +describe('JiraImportProgress', () => { + let wrapper; + + const getGlEmptyStateAttribute = attribute => wrapper.find(GlEmptyState).attributes(attribute); + + const getParagraphText = () => wrapper.find('p').text(); + + const mountComponent = ({ mountType = 'shallowMount' } = {}) => { + const mountFunction = mountType === 'shallowMount' ? shallowMount : mount; + return mountFunction(JiraImportProgress, { + propsData: { + illustration: 'illustration.svg', + importInitiator: 'Jane Doe', + importProject: 'JIRAPROJECT', + importTime: '2020-04-08T12:17:25+00:00', + issuesPath: 'gitlab-org/gitlab-test/-/issues', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('empty state', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('contains illustration', () => { + expect(getGlEmptyStateAttribute('svgpath')).toBe('illustration.svg'); + }); + + it('contains a title', () => { + const title = 'Import in progress'; + expect(getGlEmptyStateAttribute('title')).toBe(title); + }); + + it('contains button text', () => { + expect(getGlEmptyStateAttribute('primarybuttontext')).toBe('View issues'); + }); + + it('contains button url', () => { + expect(getGlEmptyStateAttribute('primarybuttonlink')).toBe('gitlab-org/gitlab-test/-/issues'); + }); + }); + + describe('description', () => { + beforeEach(() => { + wrapper = mountComponent({ mountType: 'mount' }); + }); + + it('shows who initiated the import', () => { + expect(getParagraphText()).toContain('Import started by: Jane Doe'); + }); + + it('shows the time of import', () => { + expect(getParagraphText()).toContain('Time of import: Apr 8, 2020 12:17pm GMT+0000'); + }); + + it('shows the project key of the import', () => { + expect(getParagraphText()).toContain('Jira project: JIRAPROJECT'); + }); + }); +}); diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js index 27366bd7e8a..834c14b512e 100644 --- a/spec/frontend/jira_import/components/jira_import_setup_spec.js +++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js @@ -1,9 +1,12 @@ +import { GlEmptyState } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; describe('JiraImportSetup', () => { let wrapper; + const getGlEmptyStateAttribute = attribute => wrapper.find(GlEmptyState).attributes(attribute); + beforeEach(() => { wrapper = shallowMount(JiraImportSetup, { propsData: { @@ -17,12 +20,16 @@ describe('JiraImportSetup', () => { wrapper = null; }); - it('displays a message to the user', () => { - const message = 'You will first need to set up Jira Integration to use this feature.'; - expect(wrapper.find('p').text()).toBe(message); + it('contains illustration', () => { + expect(getGlEmptyStateAttribute('svgpath')).toBe('illustration.svg'); + }); + + it('contains a description', () => { + const description = 'You will first need to set up Jira Integration to use this feature.'; + expect(getGlEmptyStateAttribute('description')).toBe(description); }); - it('contains button to set up Jira integration', () => { - expect(wrapper.find('a').text()).toBe('Set up Jira Integration'); + it('contains button text', () => { + expect(getGlEmptyStateAttribute('primarybuttontext')).toBe('Set up Jira Integration'); }); }); diff --git a/spec/frontend/jira_import/utils_spec.js b/spec/frontend/jira_import/utils_spec.js new file mode 100644 index 00000000000..a14db104229 --- /dev/null +++ b/spec/frontend/jira_import/utils_spec.js @@ -0,0 +1,27 @@ +import { IMPORT_STATE, isInProgress } from '~/jira_import/utils'; + +describe('isInProgress', () => { + it('returns true when state is IMPORT_STATE.SCHEDULED', () => { + expect(isInProgress(IMPORT_STATE.SCHEDULED)).toBe(true); + }); + + it('returns true when state is IMPORT_STATE.STARTED', () => { + expect(isInProgress(IMPORT_STATE.STARTED)).toBe(true); + }); + + it('returns false when state is IMPORT_STATE.FAILED', () => { + expect(isInProgress(IMPORT_STATE.FAILED)).toBe(false); + }); + + it('returns false when state is IMPORT_STATE.FINISHED', () => { + expect(isInProgress(IMPORT_STATE.FINISHED)).toBe(false); + }); + + it('returns false when state is IMPORT_STATE.NONE', () => { + expect(isInProgress(IMPORT_STATE.NONE)).toBe(false); + }); + + it('returns false when state is undefined', () => { + expect(isInProgress()).toBe(false); + }); +}); diff --git a/spec/frontend/logs/mock_data.js b/spec/frontend/logs/mock_data.js index 537582cff5a..14c8f7a2ba2 100644 --- a/spec/frontend/logs/mock_data.js +++ b/spec/frontend/logs/mock_data.js @@ -34,91 +34,31 @@ export const mockPods = [ export const mockLogsResult = [ { timestamp: '2019-12-13T13:43:18.2760123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13', + message: 'log line 1', pod: 'foo', }, { timestamp: '2019-12-13T13:43:18.2760123Z', - message: '- -> /', + message: 'log line A', pod: 'bar', }, { timestamp: '2019-12-13T13:43:26.8420123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13', + message: 'log line 2', pod: 'foo', }, { timestamp: '2019-12-13T13:43:26.8420123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:28.3710123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:28.3710123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:36.8860123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:36.8860123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:38.4000123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:38.4000123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:46.8420123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:46.8430123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:48.3240123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:48.3250123Z', - message: '- -> /', + message: 'log line B', pod: 'bar', }, ]; export const mockTrace = [ - 'Dec 13 13:43:18.276Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:18.276Z | bar | - -> /', - 'Dec 13 13:43:26.842Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:26.842Z | bar | - -> /', - 'Dec 13 13:43:28.371Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:28.371Z | bar | - -> /', - 'Dec 13 13:43:36.886Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:36.886Z | bar | - -> /', - 'Dec 13 13:43:38.400Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:38.400Z | bar | - -> /', - 'Dec 13 13:43:46.842Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:46.843Z | bar | - -> /', - 'Dec 13 13:43:48.324Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:48.325Z | bar | - -> /', + 'Dec 13 13:43:18.276 | foo | log line 1', + 'Dec 13 13:43:18.276 | bar | log line A', + 'Dec 13 13:43:26.842 | foo | log line 2', + 'Dec 13 13:43:26.842 | bar | log line B', ]; export const mockResponse = { 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 d968b042ff1..1906ad7c6ed 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -6,101 +6,106 @@ exports[`Dashboard template matches the default snapshot 1`] = ` data-qa-selector="prometheus_graphs" >
- - - - - +
+ +
+ - + + + Environment + + + + + + + +
+
- - Environment - - - - - - -
- -
- - No matching results -
+ No matching results +
- - - - + +
+ +
+ +
+ +
+ - - + +
+ +
+ +
+ - - - - - + + +
diff --git a/spec/frontend/monitoring/components/charts/annotations_spec.js b/spec/frontend/monitoring/components/charts/annotations_spec.js index 69bf1fe4ced..fc90175d307 100644 --- a/spec/frontend/monitoring/components/charts/annotations_spec.js +++ b/spec/frontend/monitoring/components/charts/annotations_spec.js @@ -54,6 +54,7 @@ describe('annotations spec', () => { yAxisIndex: 1, data: expect.any(Array), markLine: expect.any(Object), + markPoint: expect.any(Object), }), ); @@ -61,11 +62,12 @@ describe('annotations spec', () => { expect(annotation).toEqual(expect.any(Object)); }); - expect(annotations.data).toHaveLength(annotationsData.length); + expect(annotations.data).toHaveLength(0); expect(annotations.markLine.data).toHaveLength(annotationsData.length); + expect(annotations.markPoint.data).toHaveLength(annotationsData.length); }); - it('when deploments and annotations data is passed', () => { + it('when deployments and annotations data is passed', () => { const annotations = generateAnnotationsSeries({ deployments: deploymentData, annotations: annotationsData, @@ -77,6 +79,7 @@ describe('annotations spec', () => { yAxisIndex: 1, data: expect.any(Array), markLine: expect.any(Object), + markPoint: expect.any(Object), }), ); @@ -84,7 +87,9 @@ describe('annotations spec', () => { expect(annotation).toEqual(expect.any(Object)); }); - expect(annotations.data).toHaveLength(deploymentData.length + annotationsData.length); + expect(annotations.data).toHaveLength(deploymentData.length); + expect(annotations.markLine.data).toHaveLength(annotationsData.length); + expect(annotations.markPoint.data).toHaveLength(annotationsData.length); }); }); }); diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js index d219a6627bf..1c8fdc01e3e 100644 --- a/spec/frontend/monitoring/components/charts/options_spec.js +++ b/spec/frontend/monitoring/components/charts/options_spec.js @@ -31,7 +31,32 @@ describe('options spec', () => { }); }); - it('formatter options', () => { + it('formatter options defaults to engineering notation', () => { + const options = getYAxisOptions(); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(3002.1)).toBe('3k'); + }); + + it('formatter options allows for precision to be set explicitly', () => { + const options = getYAxisOptions({ + precision: 4, + }); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(5002.1)).toBe('5.0021k'); + }); + + it('formatter options allows for overrides in milliseconds', () => { + const options = getYAxisOptions({ + format: SUPPORTED_FORMATS.milliseconds, + }); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(1.1234)).toBe('1.12ms'); + }); + + it('formatter options allows for overrides in bytes', () => { const options = getYAxisOptions({ format: SUPPORTED_FORMATS.bytes, }); @@ -46,7 +71,7 @@ describe('options spec', () => { const formatter = getTooltipFormatter(); expect(formatter).toEqual(expect.any(Function)); - expect(formatter(1)).toBe('1.000'); + expect(formatter(0.11111)).toBe('111.1m'); }); it('defined format', () => { diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 870e47edde0..5ac716b0c63 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils'; import { setTestTimeout } from 'helpers/timeout'; import { GlLink } from '@gitlab/ui'; +import { TEST_HOST } from 'jest/helpers/test_constants'; import { GlAreaChart, GlLineChart, @@ -12,23 +13,16 @@ import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper'; import { createStore } from '~/monitoring/stores'; import TimeSeries from '~/monitoring/components/charts/time_series.vue'; import * as types from '~/monitoring/stores/mutation_types'; +import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data'; import { - deploymentData, - mockedQueryResultFixture, + metricsDashboardPayload, metricsDashboardViewModel, - mockProjectDir, - mockHost, -} from '../../mock_data'; + metricResultStatus, +} from '../../fixture_data'; import * as iconUtils from '~/lib/utils/icon_utils'; -import { getJSONFixture } from '../../../helpers/fixtures'; const mockSvgPathContent = 'mockSvgPathContent'; -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; - jest.mock('lodash/throttle', () => // this throttle mock executes immediately jest.fn(func => { @@ -51,7 +45,7 @@ describe('Time series component', () => { graphData: { ...graphData, type }, deploymentData: store.state.monitoringDashboard.deploymentData, annotations: store.state.monitoringDashboard.annotations, - projectPath: `${mockHost}${mockProjectDir}`, + projectPath: `${TEST_HOST}${mockProjectDir}`, }, store, stubs: { @@ -74,7 +68,7 @@ describe('Time series component', () => { store.commit( `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultFixture, + metricResultStatus, ); // dashboard is a dynamically generated fixture and stored at environment_metrics_dashboard.json [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[1].panels; @@ -284,6 +278,33 @@ describe('Time series component', () => { }); }); + describe('formatAnnotationsTooltipText', () => { + const annotationsMetadata = { + name: 'annotations', + xAxis: annotationsData[0].from, + yAxis: 0, + tooltipData: { + title: '2020/02/19 10:01:41', + content: annotationsData[0].description, + }, + }; + + const mockMarkPoint = { + componentType: 'markPoint', + name: 'annotations', + value: undefined, + data: annotationsMetadata, + }; + + it('formats tooltip title and sets tooltip content', () => { + const formattedTooltipData = timeSeriesChart.vm.formatAnnotationsTooltipText( + mockMarkPoint, + ); + expect(formattedTooltipData.title).toBe('19 Feb 2020, 10:01AM'); + expect(formattedTooltipData.content).toBe(annotationsMetadata.tooltipData.content); + }); + }); + describe('setSvg', () => { const mockSvgName = 'mockSvgName'; @@ -386,6 +407,8 @@ describe('Time series component', () => { series: [ { name: mockSeriesName, + type: 'line', + data: [], }, ], }, @@ -448,8 +471,8 @@ describe('Time series component', () => { deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter; }); - it('formats and rounds to 2 decimal places', () => { - expect(dataFormatter(0.88888)).toBe('0.89'); + it('formats by default to precision notation', () => { + expect(dataFormatter(0.88888)).toBe('889m'); }); it('deployment formatter is set as is required to display a tooltip', () => { @@ -606,7 +629,7 @@ describe('Time series component', () => { store = createStore(); const graphData = cloneDeep(metricsDashboardViewModel.panelGroups[0].panels[3]); graphData.metrics.forEach(metric => - Object.assign(metric, { result: mockedQueryResultFixture.result }), + Object.assign(metric, { result: metricResultStatus.result }), ); timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart'); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index f0b510a01f4..8b6ee9b3bf6 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -1,34 +1,23 @@ -import { shallowMount, createLocalVue, mount } from '@vue/test-utils'; -import { GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import Tracking from '~/tracking'; +import { GlModal, GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui'; import VueDraggable from 'vuedraggable'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; import { metricStates } from '~/monitoring/constants'; import Dashboard from '~/monitoring/components/dashboard.vue'; -import { getJSONFixture } from '../../../../spec/frontend/helpers/fixtures'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; +import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; import GroupEmptyState from '~/monitoring/components/group_empty_state.vue'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import { createStore } from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; -import { setupComponentStore, propsData } from '../init_utils'; -import { - metricsDashboardViewModel, - environmentData, - dashboardGitResponse, - mockedQueryResultFixture, -} from '../mock_data'; - -const localVue = createLocalVue(); -const expectedPanelCount = 4; - -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; +import { setupStoreWithDashboard, setMetricResult, setupStoreWithData } from '../store_utils'; +import { environmentData, dashboardGitResponse, propsData } from '../mock_data'; +import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data'; describe('Dashboard', () => { let store; @@ -43,7 +32,6 @@ describe('Dashboard', () => { const createShallowWrapper = (props = {}, options = {}) => { wrapper = shallowMount(Dashboard, { - localVue, propsData: { ...propsData, ...props }, methods: { fetchData: jest.fn(), @@ -55,7 +43,6 @@ describe('Dashboard', () => { const createMountedWrapper = (props = {}, options = {}) => { wrapper = mount(Dashboard, { - localVue, propsData: { ...propsData, ...props }, methods: { fetchData: jest.fn(), @@ -144,7 +131,7 @@ describe('Dashboard', () => { { stubs: ['graph-group', 'panel-type'] }, ); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.showEmptyState).toEqual(false); @@ -172,7 +159,7 @@ describe('Dashboard', () => { beforeEach(() => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); @@ -201,14 +188,7 @@ describe('Dashboard', () => { it('hides the environments dropdown list when there is no environments', () => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, - metricsDashboardPayload, - ); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultFixture, - ); + setupStoreWithDashboard(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { expect(findAllEnvironmentsDropdownItems()).toHaveLength(0); @@ -218,7 +198,7 @@ describe('Dashboard', () => { it('renders the datetimepicker dropdown', () => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { expect(wrapper.find(DateTimePicker).exists()).toBe(true); @@ -228,7 +208,7 @@ describe('Dashboard', () => { it('renders the refresh dashboard button', () => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' }); @@ -241,7 +221,11 @@ describe('Dashboard', () => { describe('when one of the metrics is missing', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); - setupComponentStore(wrapper); + + const { $store } = wrapper.vm; + + setupStoreWithDashboard($store); + setMetricResult({ $store, result: [], panel: 2 }); return wrapper.vm.$nextTick(); }); @@ -273,7 +257,7 @@ describe('Dashboard', () => { }, ); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); @@ -348,14 +332,14 @@ describe('Dashboard', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); it('wraps vuedraggable', () => { expect(findDraggablePanels().exists()).toBe(true); - expect(findDraggablePanels().length).toEqual(expectedPanelCount); + expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount); }); it('is disabled by default', () => { @@ -411,11 +395,11 @@ describe('Dashboard', () => { it('shows a remove button, which removes a panel', () => { expect(findFirstDraggableRemoveButton().isEmpty()).toBe(false); - expect(findDraggablePanels().length).toEqual(expectedPanelCount); + expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount); findFirstDraggableRemoveButton().trigger('click'); return wrapper.vm.$nextTick(() => { - expect(findDraggablePanels().length).toEqual(expectedPanelCount - 1); + expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1); }); }); @@ -534,7 +518,7 @@ describe('Dashboard', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true, currentDashboard }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); @@ -564,4 +548,74 @@ describe('Dashboard', () => { }); }); }); + + describe('add custom metrics', () => { + const findAddMetricButton = () => wrapper.vm.$refs.addMetricBtn; + describe('when not available', () => { + beforeEach(() => { + createShallowWrapper({ + hasMetrics: true, + customMetricsPath: '/endpoint', + }); + }); + it('does not render add button on the dashboard', () => { + expect(findAddMetricButton()).toBeUndefined(); + }); + }); + + describe('when available', () => { + let origPage; + beforeEach(done => { + jest.spyOn(Tracking, 'event').mockReturnValue(); + createShallowWrapper({ + hasMetrics: true, + customMetricsPath: '/endpoint', + customMetricsAvailable: true, + }); + setupStoreWithData(wrapper.vm.$store); + + origPage = document.body.dataset.page; + document.body.dataset.page = 'projects:environments:metrics'; + + wrapper.vm.$nextTick(done); + }); + afterEach(() => { + document.body.dataset.page = origPage; + }); + + it('renders add button on the dashboard', () => { + expect(findAddMetricButton()).toBeDefined(); + }); + + it('uses modal for custom metrics form', () => { + expect(wrapper.find(GlModal).exists()).toBe(true); + expect(wrapper.find(GlModal).attributes().modalid).toBe('add-metric'); + }); + it('adding new metric is tracked', done => { + const submitButton = wrapper.vm.$refs.submitCustomMetricsFormBtn; + wrapper.setData({ + formIsValid: true, + }); + wrapper.vm.$nextTick(() => { + submitButton.$el.click(); + wrapper.vm.$nextTick(() => { + expect(Tracking.event).toHaveBeenCalledWith( + document.body.dataset.page, + 'click_button', + { + label: 'add_new_metric', + property: 'modal', + value: undefined, + }, + ); + done(); + }); + }); + }); + + it('renders custom metrics form fields', () => { + expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js index 38523ab82bc..d1790df4189 100644 --- a/spec/frontend/monitoring/components/dashboard_template_spec.js +++ b/spec/frontend/monitoring/components/dashboard_template_spec.js @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Dashboard from '~/monitoring/components/dashboard.vue'; import { createStore } from '~/monitoring/stores'; -import { propsData } from '../init_utils'; +import { propsData } from '../mock_data'; jest.mock('~/lib/utils/url_utility'); diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js index ebfa09874fa..65e9d036d1a 100644 --- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js +++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js @@ -9,12 +9,11 @@ import { updateHistory, } from '~/lib/utils/url_utility'; import axios from '~/lib/utils/axios_utils'; -import { mockProjectDir } from '../mock_data'; +import { mockProjectDir, propsData } from '../mock_data'; import Dashboard from '~/monitoring/components/dashboard.vue'; import { createStore } from '~/monitoring/stores'; import { defaultTimeRange } from '~/vue_shared/constants'; -import { propsData } from '../init_utils'; jest.mock('~/flash'); jest.mock('~/lib/utils/url_utility'); diff --git a/spec/frontend/monitoring/components/panel_type_spec.js b/spec/frontend/monitoring/components/panel_type_spec.js index 02511ac46ea..819b5235284 100644 --- a/spec/frontend/monitoring/components/panel_type_spec.js +++ b/spec/frontend/monitoring/components/panel_type_spec.js @@ -10,17 +10,17 @@ import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; import AnomalyChart from '~/monitoring/components/charts/anomaly.vue'; import { anomalyMockGraphData, - graphDataPrometheusQueryRange, mockLogsHref, mockLogsPath, mockNamespace, mockNamespacedData, mockTimeRange, -} from 'jest/monitoring/mock_data'; +} from '../mock_data'; + +import { graphData, graphDataEmpty } from '../fixture_data'; import { createStore, monitoringDashboard } from '~/monitoring/stores'; import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group'; -global.IS_EE = true; global.URL.createObjectURL = jest.fn(); const mocks = { @@ -39,10 +39,13 @@ describe('Panel Type component', () => { const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' }); const findTimeChart = () => wrapper.find({ ref: 'timeChart' }); + const findTitle = () => wrapper.find({ ref: 'graphTitle' }); + const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' }); const createWrapper = props => { wrapper = shallowMount(PanelType, { propsData: { + graphData, ...props, }, store, @@ -64,14 +67,9 @@ describe('Panel Type component', () => { }); describe('When no graphData is available', () => { - let glEmptyChart; - // Deep clone object before modifying - const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange)); - graphDataNoResult.metrics[0].result = []; - beforeEach(() => { createWrapper({ - graphData: graphDataNoResult, + graphData: graphDataEmpty, }); }); @@ -80,12 +78,8 @@ describe('Panel Type component', () => { }); describe('Empty Chart component', () => { - beforeEach(() => { - glEmptyChart = wrapper.find(EmptyChart); - }); - it('renders the chart title', () => { - expect(wrapper.find({ ref: 'graphTitle' }).text()).toBe(graphDataNoResult.title); + expect(findTitle().text()).toBe(graphDataEmpty.title); }); it('renders the no download csv link', () => { @@ -93,26 +87,19 @@ describe('Panel Type component', () => { }); it('does not contain graph widgets', () => { - expect(wrapper.find('.js-graph-widgets').exists()).toBe(false); + expect(findContextualMenu().exists()).toBe(false); }); it('is a Vue instance', () => { - expect(glEmptyChart.isVueInstance()).toBe(true); - }); - - it('it receives a graph title', () => { - const props = glEmptyChart.props(); - - expect(props.graphTitle).toBe(wrapper.vm.graphData.title); + expect(wrapper.find(EmptyChart).exists()).toBe(true); + expect(wrapper.find(EmptyChart).isVueInstance()).toBe(true); }); }); }); describe('when graph data is available', () => { beforeEach(() => { - createWrapper({ - graphData: graphDataPrometheusQueryRange, - }); + createWrapper(); }); afterEach(() => { @@ -120,11 +107,11 @@ describe('Panel Type component', () => { }); it('renders the chart title', () => { - expect(wrapper.find({ ref: 'graphTitle' }).text()).toBe(graphDataPrometheusQueryRange.title); + expect(findTitle().text()).toBe(graphData.title); }); it('contains graph widgets', () => { - expect(wrapper.find('.js-graph-widgets').exists()).toBe(true); + expect(findContextualMenu().exists()).toBe(true); expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true); }); @@ -177,11 +164,7 @@ describe('Panel Type component', () => { const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' }); beforeEach(() => { - createWrapper({ - graphData: { - ...graphDataPrometheusQueryRange, - }, - }); + createWrapper(); return wrapper.vm.$nextTick(); }); @@ -193,10 +176,10 @@ describe('Panel Type component', () => { it('is present when the panel contains an edit_path property', () => { wrapper.setProps({ graphData: { - ...graphDataPrometheusQueryRange, + ...graphData, metrics: [ { - ...graphDataPrometheusQueryRange.metrics[0], + ...graphData.metrics[0], edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', }, ], @@ -205,23 +188,6 @@ describe('Panel Type component', () => { return wrapper.vm.$nextTick(() => { expect(findEditCustomMetricLink().exists()).toBe(true); - }); - }); - - it('shows an "Edit metric" link for a panel with a single metric', () => { - wrapper.setProps({ - graphData: { - ...graphDataPrometheusQueryRange, - metrics: [ - { - ...graphDataPrometheusQueryRange.metrics[0], - edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', - }, - ], - }, - }); - - return wrapper.vm.$nextTick(() => { expect(findEditCustomMetricLink().text()).toBe('Edit metric'); }); }); @@ -229,14 +195,14 @@ describe('Panel Type component', () => { it('shows an "Edit metrics" link for a panel with multiple metrics', () => { wrapper.setProps({ graphData: { - ...graphDataPrometheusQueryRange, + ...graphData, metrics: [ { - ...graphDataPrometheusQueryRange.metrics[0], + ...graphData.metrics[0], edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', }, { - ...graphDataPrometheusQueryRange.metrics[0], + ...graphData.metrics[0], edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', }, ], @@ -253,9 +219,7 @@ describe('Panel Type component', () => { const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' }); beforeEach(() => { - createWrapper({ - graphData: graphDataPrometheusQueryRange, - }); + createWrapper(); return wrapper.vm.$nextTick(); }); @@ -327,7 +291,6 @@ describe('Panel Type component', () => { beforeEach(() => { createWrapper({ clipboardText, - graphData: graphDataPrometheusQueryRange, }); }); @@ -353,11 +316,13 @@ describe('Panel Type component', () => { describe('when downloading metrics data as CSV', () => { beforeEach(() => { - graphDataPrometheusQueryRange.y_label = 'metric'; wrapper = shallowMount(PanelType, { propsData: { clipboardText: exampleText, - graphData: graphDataPrometheusQueryRange, + graphData: { + y_label: 'metric', + ...graphData, + }, }, store, }); @@ -370,12 +335,12 @@ describe('Panel Type component', () => { describe('csvText', () => { it('converts metrics data from json to csv', () => { - const header = `timestamp,${graphDataPrometheusQueryRange.y_label}`; - const data = graphDataPrometheusQueryRange.metrics[0].result[0].values; + const header = `timestamp,${graphData.y_label}`; + const data = graphData.metrics[0].result[0].values; const firstRow = `${data[0][0]},${data[0][1]}`; const secondRow = `${data[1][0]},${data[1][1]}`; - expect(wrapper.vm.csvText).toBe(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`); + expect(wrapper.vm.csvText).toMatch(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`); }); }); @@ -402,7 +367,7 @@ describe('Panel Type component', () => { wrapper = shallowMount(PanelType, { propsData: { - graphData: graphDataPrometheusQueryRange, + graphData, namespace: mockNamespace, }, store, diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js new file mode 100644 index 00000000000..b7b72a15992 --- /dev/null +++ b/spec/frontend/monitoring/fixture_data.js @@ -0,0 +1,49 @@ +import { mapToDashboardViewModel } from '~/monitoring/stores/utils'; +import { metricStates } from '~/monitoring/constants'; + +import { metricsResult } from './mock_data'; + +// Use globally available `getJSONFixture` so this file can be imported by both karma and jest specs +export const metricsDashboardResponse = getJSONFixture( + 'metrics_dashboard/environment_metrics_dashboard.json', +); +export const metricsDashboardPayload = metricsDashboardResponse.dashboard; +export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload); + +export const metricsDashboardPanelCount = 22; +export const metricResultStatus = { + // First metric in fixture `metrics_dashboard/environment_metrics_dashboard.json` + metricId: 'NO_DB_response_metrics_nginx_ingress_throughput_status_code', + result: metricsResult, +}; +export const metricResultPods = { + // Second metric in fixture `metrics_dashboard/environment_metrics_dashboard.json` + metricId: 'NO_DB_response_metrics_nginx_ingress_latency_pod_average', + result: metricsResult, +}; +export const metricResultEmpty = { + metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code', + result: [], +}; + +// Graph data + +const firstPanel = metricsDashboardViewModel.panelGroups[0].panels[0]; + +export const graphData = { + ...firstPanel, + metrics: firstPanel.metrics.map(metric => ({ + ...metric, + result: metricsResult, + state: metricStates.OK, + })), +}; + +export const graphDataEmpty = { + ...firstPanel, + metrics: firstPanel.metrics.map(metric => ({ + ...metric, + result: [], + state: metricStates.NO_DATA, + })), +}; diff --git a/spec/frontend/monitoring/init_utils.js b/spec/frontend/monitoring/init_utils.js deleted file mode 100644 index 55b6199fdfc..00000000000 --- a/spec/frontend/monitoring/init_utils.js +++ /dev/null @@ -1,57 +0,0 @@ -import * as types from '~/monitoring/stores/mutation_types'; -import { - metricsDashboardPayload, - mockedEmptyResult, - mockedQueryResultPayload, - mockedQueryResultPayloadCoresTotal, - mockApiEndpoint, - environmentData, -} from './mock_data'; - -export const propsData = { - hasMetrics: false, - documentationPath: '/path/to/docs', - settingsPath: '/path/to/settings', - clustersPath: '/path/to/clusters', - tagsPath: '/path/to/tags', - projectPath: '/path/to/project', - logsPath: '/path/to/logs', - defaultBranch: 'master', - metricsEndpoint: mockApiEndpoint, - deploymentsEndpoint: null, - emptyGettingStartedSvgPath: '/path/to/getting-started.svg', - emptyLoadingSvgPath: '/path/to/loading.svg', - emptyNoDataSvgPath: '/path/to/no-data.svg', - emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg', - emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', - currentEnvironmentName: 'production', - customMetricsAvailable: false, - customMetricsPath: '', - validateQueryPath: '', -}; - -export const setupComponentStore = wrapper => { - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, - metricsDashboardPayload, - ); - - // Load 3 panels to the dashboard, one with an empty result - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedEmptyResult, - ); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultPayload, - ); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultPayloadCoresTotal, - ); - - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, - environmentData, - ); -}; diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 84dd0b70e71..56236918c68 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -1,13 +1,47 @@ -import { mapToDashboardViewModel } from '~/monitoring/stores/utils'; - // This import path needs to be relative for now because this mock data is used in // Karma specs too, where the helpers/test_constants alias can not be resolved import { TEST_HOST } from '../helpers/test_constants'; -export const mockHost = 'http://test.host'; export const mockProjectDir = '/frontend-fixtures/environments-project'; export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`; +export const propsData = { + hasMetrics: false, + documentationPath: '/path/to/docs', + settingsPath: '/path/to/settings', + clustersPath: '/path/to/clusters', + tagsPath: '/path/to/tags', + projectPath: '/path/to/project', + logsPath: '/path/to/logs', + defaultBranch: 'master', + metricsEndpoint: mockApiEndpoint, + deploymentsEndpoint: null, + emptyGettingStartedSvgPath: '/path/to/getting-started.svg', + emptyLoadingSvgPath: '/path/to/loading.svg', + emptyNoDataSvgPath: '/path/to/no-data.svg', + emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg', + emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', + currentEnvironmentName: 'production', + customMetricsAvailable: false, + customMetricsPath: '', + validateQueryPath: '', +}; + +const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({ + default: false, + display_name: `Custom Dashboard ${idx}`, + can_edit: true, + system_dashboard: false, + project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`, + path: `.gitlab/dashboards/dashboard_${idx}.yml`, +})); + +export const mockDashboardsErrorResponse = { + all_dashboards: customDashboardsData, + message: "Each 'panel_group' must define an array :panels", + status: 'error', +}; + export const anomalyDeploymentData = [ { id: 111, @@ -213,130 +247,27 @@ export const deploymentData = [ export const annotationsData = [ { id: 'gid://gitlab/Metrics::Dashboard::Annotation/1', - starting_at: '2020-04-01T12:51:58.373Z', - ending_at: null, + startingAt: '2020-04-12 12:51:53 UTC', + endingAt: null, panelId: null, description: 'This is a test annotation', }, { id: 'gid://gitlab/Metrics::Dashboard::Annotation/2', description: 'test annotation 2', - starting_at: '2020-04-02T12:51:58.373Z', - ending_at: null, + startingAt: '2020-04-13 12:51:53 UTC', + endingAt: null, panelId: null, }, { id: 'gid://gitlab/Metrics::Dashboard::Annotation/3', description: 'test annotation 3', - starting_at: '2020-04-04T12:51:58.373Z', - ending_at: null, + startingAt: '2020-04-16 12:51:53 UTC', + endingAt: null, panelId: null, }, ]; -export const metricsNewGroupsAPIResponse = [ - { - group: 'System metrics (Kubernetes)', - priority: 5, - panels: [ - { - title: 'Memory Usage (Pod average)', - type: 'area-chart', - y_label: 'Memory Used per Pod', - weight: 2, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_average', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024', - label: 'Pod average', - unit: 'MB', - metric_id: 17, - prometheus_endpoint_path: - '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024', - appearance: { - line: { - width: 2, - }, - }, - }, - ], - }, - ], - }, -]; - -const metricsResult = [ - { - metric: {}, - values: [ - [1563272065.589, '10.396484375'], - [1563272125.589, '10.333984375'], - [1563272185.589, '10.333984375'], - [1563272245.589, '10.333984375'], - [1563272305.589, '10.333984375'], - [1563272365.589, '10.333984375'], - [1563272425.589, '10.38671875'], - [1563272485.589, '10.333984375'], - [1563272545.589, '10.333984375'], - [1563272605.589, '10.333984375'], - [1563272665.589, '10.333984375'], - [1563272725.589, '10.333984375'], - [1563272785.589, '10.396484375'], - [1563272845.589, '10.333984375'], - [1563272905.589, '10.333984375'], - [1563272965.589, '10.3984375'], - [1563273025.589, '10.337890625'], - [1563273085.589, '10.34765625'], - [1563273145.589, '10.337890625'], - [1563273205.589, '10.337890625'], - [1563273265.589, '10.337890625'], - [1563273325.589, '10.337890625'], - [1563273385.589, '10.337890625'], - [1563273445.589, '10.337890625'], - [1563273505.589, '10.337890625'], - [1563273565.589, '10.337890625'], - [1563273625.589, '10.337890625'], - [1563273685.589, '10.337890625'], - [1563273745.589, '10.337890625'], - [1563273805.589, '10.337890625'], - [1563273865.589, '10.390625'], - [1563273925.589, '10.390625'], - ], - }, -]; - -export const mockedEmptyResult = { - metricId: '1_response_metrics_nginx_ingress_throughput_status_code', - result: [], -}; - -export const mockedEmptyThroughputResult = { - metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code', - result: [], -}; - -export const mockedQueryResultPayload = { - metricId: '12_system_metrics_kubernetes_container_memory_total', - result: metricsResult, -}; - -export const mockedQueryResultPayloadCoresTotal = { - metricId: '13_system_metrics_kubernetes_container_cores_total', - result: metricsResult, -}; - -export const mockedQueryResultFixture = { - // First metric in fixture `metrics_dashboard/environment_metrics_dashboard.json` - metricId: 'NO_DB_response_metrics_nginx_ingress_throughput_status_code', - result: metricsResult, -}; - -export const mockedQueryResultFixtureStatusCode = { - metricId: 'NO_DB_response_metrics_nginx_ingress_latency_pod_average', - result: metricsResult, -}; - const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({ id: `gid://gitlab/Environments/${150 + idx}`, name: `no-deployment/noop-branch-${idx}`, @@ -384,158 +315,6 @@ export const environmentData = [ }, ].concat(extraEnvironmentData); -export const metricsDashboardPayload = { - dashboard: 'Environment metrics', - priority: 1, - panel_groups: [ - { - group: 'System metrics (Kubernetes)', - priority: 5, - panels: [ - { - title: 'Memory Usage (Total)', - type: 'area-chart', - y_label: 'Total Memory Used', - weight: 4, - y_axis: { - format: 'megabytes', - }, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_total', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1000/1000', - label: 'Total', - unit: 'MB', - metric_id: 12, - prometheus_endpoint_path: 'http://test', - }, - ], - }, - { - title: 'Core Usage (Total)', - type: 'area-chart', - y_label: 'Total Cores', - weight: 3, - metrics: [ - { - id: 'system_metrics_kubernetes_container_cores_total', - query_range: - 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)', - label: 'Total', - unit: 'cores', - metric_id: 13, - }, - ], - }, - { - title: 'Memory Usage (Pod average)', - type: 'line-chart', - y_label: 'Memory Used per Pod', - weight: 2, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_average', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024', - label: 'Pod average', - unit: 'MB', - metric_id: 14, - }, - ], - }, - { - title: 'memories', - type: 'area-chart', - y_label: 'memories', - metrics: [ - { - id: 'metric_of_ages_1000', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 20, - }, - { - id: 'metric_of_ages_1001', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 21, - }, - { - id: 'metric_of_ages_1002', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 22, - }, - { - id: 'metric_of_ages_1003', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 23, - }, - { - id: 'metric_of_ages_1004', - label: 'memory_1004', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 24, - }, - ], - }, - ], - }, - { - group: 'Response metrics (NGINX Ingress VTS)', - priority: 10, - panels: [ - { - metrics: [ - { - id: 'response_metrics_nginx_ingress_throughput_status_code', - label: 'Status Code', - metric_id: 1, - prometheus_endpoint_path: - '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29', - query_range: - 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)', - unit: 'req / sec', - }, - ], - title: 'Throughput', - type: 'area-chart', - weight: 1, - y_label: 'Requests / Sec', - }, - ], - }, - ], -}; - -/** - * Mock of response of metrics_dashboard.json - */ -export const metricsDashboardResponse = { - all_dashboards: [], - dashboard: metricsDashboardPayload, - metrics_data: {}, - status: 'success', -}; - -export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload); - -const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({ - default: false, - display_name: `Custom Dashboard ${idx}`, - can_edit: true, - system_dashboard: false, - project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`, - path: `.gitlab/dashboards/dashboard_${idx}.yml`, -})); - export const dashboardGitResponse = [ { default: true, @@ -548,11 +327,19 @@ export const dashboardGitResponse = [ ...customDashboardsData, ]; -export const mockDashboardsErrorResponse = { - all_dashboards: customDashboardsData, - message: "Each 'panel_group' must define an array :panels", - status: 'error', -}; +// Metrics mocks + +export const metricsResult = [ + { + metric: {}, + values: [ + [1563272065.589, '10.396484375'], + [1563272125.589, '10.333984375'], + [1563272185.589, '10.333984375'], + [1563272245.589, '10.333984375'], + ], + }, +]; export const graphDataPrometheusQuery = { title: 'Super Chart A2', @@ -578,29 +365,6 @@ export const graphDataPrometheusQuery = { ], }; -export const graphDataPrometheusQueryRange = { - title: 'Super Chart A1', - type: 'area-chart', - weight: 2, - metrics: [ - { - metricId: '2_metric_a', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', - unit: 'MB', - label: 'Total Consumption', - prometheus_endpoint_path: - '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', - result: [ - { - metric: {}, - values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']], - }, - ], - }, - ], -}; - export const graphDataPrometheusQueryRangeMultiTrack = { title: 'Super Chart A3', type: 'heatmap', diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index c34a5afceb0..f312aa1fd34 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -23,7 +23,11 @@ import { setGettingStartedEmptyState, duplicateSystemDashboard, } from '~/monitoring/stores/actions'; -import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils'; +import { + gqClient, + parseEnvironmentsResponse, + parseAnnotationsResponse, +} from '~/monitoring/stores/utils'; import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql'; import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql'; import storeState from '~/monitoring/stores/state'; @@ -31,11 +35,14 @@ import { deploymentData, environmentData, annotationsData, - metricsDashboardResponse, - metricsDashboardViewModel, dashboardGitResponse, mockDashboardsErrorResponse, } from '../mock_data'; +import { + metricsDashboardResponse, + metricsDashboardViewModel, + metricsDashboardPanelCount, +} from '../fixture_data'; jest.mock('~/flash'); @@ -221,6 +228,10 @@ describe('Monitoring store actions', () => { describe('fetchAnnotations', () => { const { state } = store; + state.timeRange = { + start: '2020-04-15T12:54:32.137Z', + end: '2020-08-15T12:54:32.137Z', + }; state.projectPath = 'gitlab-org/gitlab-test'; state.currentEnvironmentName = 'production'; state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml'; @@ -236,17 +247,25 @@ describe('Monitoring store actions', () => { variables: { projectPath: state.projectPath, environmentName: state.currentEnvironmentName, - dashboardId: state.currentDashboard, + dashboardPath: state.currentDashboard, + startingFrom: state.timeRange.start, }, }; + const parsedResponse = parseAnnotationsResponse(annotationsData); mockMutate.mockResolvedValue({ data: { project: { - environment: { - metricDashboard: { - annotations: annotationsData, - }, + environments: { + nodes: [ + { + metricsDashboard: { + annotations: { + nodes: parsedResponse, + }, + }, + }, + ], }, }, }, @@ -257,10 +276,7 @@ describe('Monitoring store actions', () => { null, state, [], - [ - { type: 'requestAnnotations' }, - { type: 'receiveAnnotationsSuccess', payload: annotationsData }, - ], + [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }], () => { expect(mockMutate).toHaveBeenCalledWith(mutationVariables); }, @@ -274,7 +290,8 @@ describe('Monitoring store actions', () => { variables: { projectPath: state.projectPath, environmentName: state.currentEnvironmentName, - dashboardId: state.currentDashboard, + dashboardPath: state.currentDashboard, + startingFrom: state.timeRange.start, }, }; @@ -285,7 +302,7 @@ describe('Monitoring store actions', () => { null, state, [], - [{ type: 'requestAnnotations' }, { type: 'receiveAnnotationsFailure' }], + [{ type: 'receiveAnnotationsFailure' }], () => { expect(mockMutate).toHaveBeenCalledWith(mutationVariables); }, @@ -553,7 +570,7 @@ describe('Monitoring store actions', () => { fetchDashboardData({ state, commit, dispatch }) .then(() => { - expect(dispatch).toHaveBeenCalledTimes(10); // one per metric plus 1 for deployments + expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 1); // plus 1 for deployments expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData'); expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { metric, @@ -581,11 +598,13 @@ describe('Monitoring store actions', () => { let metric; let state; let data; + let prometheusEndpointPath; beforeEach(() => { state = storeState(); - [metric] = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics; - metric = convertObjectPropsToCamelCase(metric, { deep: true }); + [metric] = metricsDashboardViewModel.panelGroups[0].panels[0].metrics; + + prometheusEndpointPath = metric.prometheusEndpointPath; data = { metricId: metric.metricId, @@ -594,7 +613,7 @@ describe('Monitoring store actions', () => { }); it('commits result', done => { - mock.onGet('http://test').reply(200, { data }); // One attempt + mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt testAction( fetchPrometheusMetric, @@ -631,7 +650,7 @@ describe('Monitoring store actions', () => { }; it('uses calculated step', done => { - mock.onGet('http://test').reply(200, { data }); // One attempt + mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt testAction( fetchPrometheusMetric, @@ -673,7 +692,7 @@ describe('Monitoring store actions', () => { }; it('uses metric step', done => { - mock.onGet('http://test').reply(200, { data }); // One attempt + mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt testAction( fetchPrometheusMetric, @@ -705,10 +724,10 @@ describe('Monitoring store actions', () => { it('commits result, when waiting for results', done => { // Mock multiple attempts while the cache is filling up - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').reply(200, { data }); // 4th attempt + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).reply(200, { data }); // 4th attempt testAction( fetchPrometheusMetric, @@ -739,10 +758,10 @@ describe('Monitoring store actions', () => { it('commits failure, when waiting for results and getting a server error', done => { // Mock multiple attempts while the cache is filling up and fails - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').reply(500); // 4th attempt + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).reply(500); // 4th attempt const error = new Error('Request failed with status code 500'); diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js index 40341d32cf5..f040876b832 100644 --- a/spec/frontend/monitoring/store/getters_spec.js +++ b/spec/frontend/monitoring/store/getters_spec.js @@ -3,18 +3,13 @@ import * as getters from '~/monitoring/stores/getters'; import mutations from '~/monitoring/stores/mutations'; import * as types from '~/monitoring/stores/mutation_types'; import { metricStates } from '~/monitoring/constants'; +import { environmentData, metricsResult } from '../mock_data'; import { - environmentData, - mockedEmptyThroughputResult, - mockedQueryResultFixture, - mockedQueryResultFixtureStatusCode, -} from '../mock_data'; -import { getJSONFixture } from '../../helpers/fixtures'; - -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; + metricsDashboardPayload, + metricResultStatus, + metricResultPods, + metricResultEmpty, +} from '../fixture_data'; describe('Monitoring store Getters', () => { describe('getMetricStates', () => { @@ -22,6 +17,21 @@ describe('Monitoring store Getters', () => { let state; let getMetricStates; + const setMetricSuccess = ({ result = metricsResult, group = 0, panel = 0, metric = 0 }) => { + const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric]; + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { + metricId, + result, + }); + }; + + const setMetricFailure = ({ group = 0, panel = 0, metric = 0 }) => { + const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric]; + mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { + metricId, + }); + }; + beforeEach(() => { setupState = (initState = {}) => { state = initState; @@ -61,31 +71,30 @@ describe('Monitoring store Getters', () => { it('on an empty metric with no result, returns NO_DATA', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyThroughputResult); + setMetricSuccess({ result: [], group: 2 }); expect(getMetricStates()).toEqual([metricStates.NO_DATA]); }); it('on a metric with a result, returns OK', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); + setMetricSuccess({ group: 1 }); expect(getMetricStates()).toEqual([metricStates.OK]); }); it('on a metric with an error, returns an error', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[0].metrics[0].metricId, - }); + setMetricFailure({}); expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]); }); it('on multiple metrics with results, returns OK', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); + + setMetricSuccess({ group: 1 }); + setMetricSuccess({ group: 1, panel: 1 }); expect(getMetricStates()).toEqual([metricStates.OK]); @@ -96,15 +105,8 @@ describe('Monitoring store Getters', () => { it('on multiple metrics errors', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[0].metrics[0].metricId, - }); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[0].metrics[0].metricId, - }); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[0].metrics[0].metricId, - }); + setMetricFailure({}); + setMetricFailure({ group: 1 }); // Entire dashboard fails expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]); @@ -116,14 +118,11 @@ describe('Monitoring store Getters', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); // An success in 1 group - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); + setMetricSuccess({ group: 1 }); + // An error in 2 groups - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[1].metrics[0].metricId, - }); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[2].panels[0].metrics[0].metricId, - }); + setMetricFailure({ group: 1, panel: 1 }); + setMetricFailure({ group: 2, panel: 0 }); expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]); expect(getMetricStates(groups[1].key)).toEqual([ @@ -182,38 +181,35 @@ describe('Monitoring store Getters', () => { it('an empty metric, returns empty', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyThroughputResult); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultEmpty); expect(metricsWithData()).toEqual([]); }); it('a metric with results, it returns a metric', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus); - expect(metricsWithData()).toEqual([mockedQueryResultFixture.metricId]); + expect(metricsWithData()).toEqual([metricResultStatus.metricId]); }); it('multiple metrics with results, it return multiple metrics', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods); - expect(metricsWithData()).toEqual([ - mockedQueryResultFixture.metricId, - mockedQueryResultFixtureStatusCode.metricId, - ]); + expect(metricsWithData()).toEqual([metricResultStatus.metricId, metricResultPods.metricId]); }); it('multiple metrics with results, it returns metrics filtered by group', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods); // First group has metrics expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([ - mockedQueryResultFixture.metricId, - mockedQueryResultFixtureStatusCode.metricId, + metricResultStatus.metricId, + metricResultPods.metricId, ]); // Second group has no metrics diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index 34d224e13b0..1452e9bc491 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -6,12 +6,7 @@ import state from '~/monitoring/stores/state'; import { metricStates } from '~/monitoring/constants'; import { deploymentData, dashboardGitResponse } from '../mock_data'; -import { getJSONFixture } from '../../helpers/fixtures'; - -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; +import { metricsDashboardPayload } from '../fixture_data'; describe('Monitoring mutations', () => { let stateCopy; diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index f46409e8e32..7ee2a16b4bd 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -2,9 +2,11 @@ import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { uniqMetricsId, parseEnvironmentsResponse, + parseAnnotationsResponse, removeLeadingSlash, mapToDashboardViewModel, } from '~/monitoring/stores/utils'; +import { annotationsData } from '../mock_data'; import { NOT_IN_DB_PREFIX } from '~/monitoring/constants'; const projectPath = 'gitlab-org/gitlab-test'; @@ -56,7 +58,7 @@ describe('mapToDashboardViewModel', () => { y_label: 'Y Label A', yAxis: { name: 'Y Label A', - format: 'number', + format: 'engineering', precision: 2, }, metrics: [], @@ -138,7 +140,7 @@ describe('mapToDashboardViewModel', () => { y_label: '', yAxis: { name: '', - format: SUPPORTED_FORMATS.number, + format: SUPPORTED_FORMATS.engineering, precision: 2, }, metrics: [], @@ -159,7 +161,7 @@ describe('mapToDashboardViewModel', () => { }, yAxis: { name: '', - format: SUPPORTED_FORMATS.number, + format: SUPPORTED_FORMATS.engineering, precision: 2, }, metrics: [], @@ -219,7 +221,7 @@ describe('mapToDashboardViewModel', () => { }, }); - expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.number); + expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.engineering); }); // This property allows single_stat panels to render percentile values @@ -376,6 +378,27 @@ describe('parseEnvironmentsResponse', () => { }); }); +describe('parseAnnotationsResponse', () => { + const parsedAnnotationResponse = [ + { + description: 'This is a test annotation', + endingAt: null, + id: 'gid://gitlab/Metrics::Dashboard::Annotation/1', + panelId: null, + startingAt: new Date('2020-04-12T12:51:53.000Z'), + }, + ]; + it.each` + case | input | expected + ${'Returns empty array for null input'} | ${null} | ${[]} + ${'Returns empty array for undefined input'} | ${undefined} | ${[]} + ${'Returns empty array for empty input'} | ${[]} | ${[]} + ${'Returns parsed responses for annotations data'} | ${[annotationsData[0]]} | ${parsedAnnotationResponse} + `('$case', ({ input, expected }) => { + expect(parseAnnotationsResponse(input)).toEqual(expected); + }); +}); + describe('removeLeadingSlash', () => { [ { input: null, output: '' }, diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js new file mode 100644 index 00000000000..d764a79ccc3 --- /dev/null +++ b/spec/frontend/monitoring/store_utils.js @@ -0,0 +1,34 @@ +import * as types from '~/monitoring/stores/mutation_types'; +import { metricsResult, environmentData } from './mock_data'; +import { metricsDashboardPayload } from './fixture_data'; + +export const setMetricResult = ({ $store, result, group = 0, panel = 0, metric = 0 }) => { + const { dashboard } = $store.state.monitoringDashboard; + const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric]; + + $store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, { + metricId, + result, + }); +}; + +const setEnvironmentData = $store => { + $store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData); +}; + +export const setupStoreWithDashboard = $store => { + $store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, + metricsDashboardPayload, + ); +}; + +export const setupStoreWithData = $store => { + setupStoreWithDashboard($store); + + setMetricResult({ $store, result: [], panel: 0 }); + setMetricResult({ $store, result: metricsResult, panel: 1 }); + setMetricResult({ $store, result: metricsResult, panel: 2 }); + + setEnvironmentData($store); +}; diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 262b8b985cc..0bb1b987b2e 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -1,17 +1,17 @@ import * as monitoringUtils from '~/monitoring/utils'; import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; +import { TEST_HOST } from 'jest/helpers/test_constants'; import { - mockHost, mockProjectDir, graphDataPrometheusQuery, - graphDataPrometheusQueryRange, anomalyMockGraphData, barMockData, } from './mock_data'; +import { graphData } from './fixture_data'; jest.mock('~/lib/utils/url_utility'); -const mockPath = `${mockHost}${mockProjectDir}/-/environments/29/metrics`; +const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`; const generatedLink = 'http://chart.link.com'; @@ -101,10 +101,7 @@ describe('monitoring/utils', () => { * the validator will look for the `values` key instead of `value` */ it('validates data with the query_range format', () => { - const validGraphData = monitoringUtils.graphDataValidatorForValues( - false, - graphDataPrometheusQueryRange, - ); + const validGraphData = monitoringUtils.graphDataValidatorForValues(false, graphData); expect(validGraphData).toBe(true); }); diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index d3932ca09ff..9c292fa0f2b 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -55,7 +55,12 @@ describe('Settings Panel', () => { currentSettings: { ...defaultProps.currentSettings, ...currentSettings }, }; - return mountFn(settingsPanel, { propsData }); + return mountFn(settingsPanel, { + propsData, + provide: { + glFeatures: { metricsDashboardVisibilitySwitchingAvailable: true }, + }, + }); }; const overrideCurrentSettings = (currentSettingsProps, extraProps = {}) => { @@ -471,4 +476,28 @@ describe('Settings Panel', () => { }); }); }); + + describe('Metrics dashboard', () => { + it('should show the metrics dashboard access toggle', () => { + return wrapper.vm.$nextTick(() => { + expect(wrapper.find({ ref: 'metrics-visibility-settings' }).exists()).toBe(true); + }); + }); + + it('should set the visibility level description based upon the selected visibility level', () => { + wrapper + .find('[name="project[project_feature_attributes][metrics_dashboard_access_level]"]') + .setValue(visibilityOptions.PUBLIC); + + expect(wrapper.vm.metricsAccessLevel).toBe(visibilityOptions.PUBLIC); + }); + + it('should contain help text', () => { + wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE }); + + expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toEqual( + 'With Metrics Dashboard you can visualize this project performance metrics', + ); + }); + }); }); diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js index 43da6388efa..3c5938cfa1f 100644 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ b/spec/frontend/pipelines/graph/action_component_spec.js @@ -7,6 +7,7 @@ import ActionComponent from '~/pipelines/components/graph/action_component.vue'; describe('pipeline graph action component', () => { let wrapper; let mock; + const findButton = () => wrapper.find('button'); beforeEach(() => { mock = new MockAdapter(axios); @@ -44,15 +45,15 @@ describe('pipeline graph action component', () => { }); it('should render an svg', () => { - expect(wrapper.find('.ci-action-icon-wrapper')).toBeDefined(); - expect(wrapper.find('svg')).toBeDefined(); + expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); + expect(wrapper.find('svg').exists()).toBe(true); }); describe('on click', () => { it('emits `pipelineActionRequestComplete` after a successful request', done => { jest.spyOn(wrapper.vm, '$emit'); - wrapper.find('button').trigger('click'); + findButton().trigger('click'); waitForPromises() .then(() => { @@ -63,7 +64,7 @@ describe('pipeline graph action component', () => { }); it('renders a loading icon while waiting for request', done => { - wrapper.find('button').trigger('click'); + findButton().trigger('click'); wrapper.vm.$nextTick(() => { expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true); diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js new file mode 100644 index 00000000000..a9b06eab3fa --- /dev/null +++ b/spec/frontend/pipelines/graph/graph_component_spec.js @@ -0,0 +1,305 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import PipelineStore from '~/pipelines/stores/pipeline_store'; +import graphComponent from '~/pipelines/components/graph/graph_component.vue'; +import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; +import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; +import graphJSON from './mock_data'; +import linkedPipelineJSON from './linked_pipelines_mock_data'; +import PipelinesMediator from '~/pipelines/pipeline_details_mediator'; + +describe('graph component', () => { + const store = new PipelineStore(); + store.storePipeline(linkedPipelineJSON); + const mediator = new PipelinesMediator({ endpoint: '' }); + + let wrapper; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('while is loading', () => { + it('should render a loading icon', () => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: true, + pipeline: {}, + mediator, + }, + }); + + expect(wrapper.find('.gl-spinner').exists()).toBe(true); + }); + }); + + describe('with data', () => { + it('should render the graph', () => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: graphJSON, + mediator, + }, + }); + + expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true); + + expect(wrapper.find(stageColumnComponent).classes()).toContain('no-margin'); + + expect( + wrapper + .findAll(stageColumnComponent) + .at(1) + .classes(), + ).toContain('left-margin'); + + expect(wrapper.find('.stage-column:nth-child(2) .build:nth-child(1)').classes()).toContain( + 'left-connector', + ); + + expect(wrapper.find('.loading-icon').exists()).toBe(false); + + expect(wrapper.find('.stage-column-list').exists()).toBe(true); + }); + }); + + describe('when linked pipelines are present', () => { + beforeEach(() => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + }); + + describe('rendered output', () => { + it('should include the pipelines graph', () => { + expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true); + }); + + it('should not include the loading icon', () => { + expect(wrapper.find('.fa-spinner').exists()).toBe(false); + }); + + it('should include the stage column list', () => { + expect(wrapper.find(stageColumnComponent).exists()).toBe(true); + }); + + it('should include the no-margin class on the first child if there is only one job', () => { + const firstStageColumnElement = wrapper.find(stageColumnComponent); + + expect(firstStageColumnElement.classes()).toContain('no-margin'); + }); + + it('should include the has-only-one-job class on the first child', () => { + const firstStageColumnElement = wrapper.find('.stage-column-list .stage-column'); + + expect(firstStageColumnElement.classes()).toContain('has-only-one-job'); + }); + + it('should include the left-margin class on the second child', () => { + const firstStageColumnElement = wrapper.find('.stage-column-list .stage-column:last-child'); + + expect(firstStageColumnElement.classes()).toContain('left-margin'); + }); + + it('should include the js-has-linked-pipelines flag', () => { + expect(wrapper.find('.js-has-linked-pipelines').exists()).toBe(true); + }); + }); + + describe('computeds and methods', () => { + describe('capitalizeStageName', () => { + it('it capitalizes the stage name', () => { + expect( + wrapper + .findAll('.stage-column .stage-name') + .at(1) + .text(), + ).toBe('Prebuild'); + }); + }); + + describe('stageConnectorClass', () => { + it('it returns left-margin when there is a triggerer', () => { + expect( + wrapper + .findAll(stageColumnComponent) + .at(1) + .classes(), + ).toContain('left-margin'); + }); + }); + }); + + describe('linked pipelines components', () => { + beforeEach(() => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + }); + + it('should render an upstream pipelines column at first position', () => { + expect(wrapper.find(linkedPipelinesColumn).exists()).toBe(true); + expect(wrapper.find('.stage-column .stage-name').text()).toBe('Upstream'); + }); + + it('should render a downstream pipelines column at last position', () => { + const stageColumnNames = wrapper.findAll('.stage-column .stage-name'); + + expect(wrapper.find(linkedPipelinesColumn).exists()).toBe(true); + expect(stageColumnNames.at(stageColumnNames.length - 1).text()).toBe('Downstream'); + }); + + describe('triggered by', () => { + describe('on click', () => { + it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => { + const btnWrapper = wrapper.find('.linked-pipeline-content'); + + btnWrapper.trigger('click'); + + btnWrapper.vm.$nextTick(() => { + expect(wrapper.emitted().onClickTriggeredBy).toEqual([ + store.state.pipeline.triggered_by, + ]); + }); + }); + }); + + describe('with expanded pipeline', () => { + it('should render expanded pipeline', done => { + // expand the pipeline + store.state.pipeline.triggered_by[0].isExpanded = true; + + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + + Vue.nextTick() + .then(() => { + expect(wrapper.find('.js-upstream-pipeline-12').exists()).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('triggered', () => { + describe('on click', () => { + it('should emit `onClickTriggered`', () => { + // We have to mock this method since we do both style change and + // emit and event, not mocking returns an error. + wrapper.setMethods({ + handleClickedDownstream: jest.fn(() => + wrapper.vm.$emit('onClickTriggered', ...store.state.pipeline.triggered), + ), + }); + + const btnWrappers = wrapper.findAll('.linked-pipeline-content'); + const downstreamBtnWrapper = btnWrappers.at(btnWrappers.length - 1); + + downstreamBtnWrapper.trigger('click'); + + downstreamBtnWrapper.vm.$nextTick(() => { + expect(wrapper.emitted().onClickTriggered).toEqual([store.state.pipeline.triggered]); + }); + }); + }); + + describe('with expanded pipeline', () => { + it('should render expanded pipeline', done => { + // expand the pipeline + store.state.pipeline.triggered[0].isExpanded = true; + + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + + Vue.nextTick() + .then(() => { + expect(wrapper.find('.js-downstream-pipeline-34993051')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + }); + }); + + describe('when linked pipelines are not present', () => { + beforeEach(() => { + const pipeline = Object.assign(linkedPipelineJSON, { triggered: null, triggered_by: null }); + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline, + mediator, + }, + }); + }); + + describe('rendered output', () => { + it('should include the first column with a no margin', () => { + const firstColumn = wrapper.find('.stage-column'); + + expect(firstColumn.classes()).toContain('no-margin'); + }); + + it('should not render a linked pipelines column', () => { + expect(wrapper.find('.linked-pipelines-column').exists()).toBe(false); + }); + }); + + describe('stageConnectorClass', () => { + it('it returns no-margin when no triggerer and there is one job', () => { + expect(wrapper.find(stageColumnComponent).classes()).toContain('no-margin'); + }); + + it('it returns left-margin when no triggerer and not the first stage', () => { + expect( + wrapper + .findAll(stageColumnComponent) + .at(1) + .classes(), + ).toContain('left-margin'); + }); + }); + }); + + describe('capitalizeStageName', () => { + it('capitalizes and escapes stage name', () => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: graphJSON, + mediator, + }, + }); + + expect( + wrapper + .find('.stage-column:nth-child(2) .stage-name') + .text() + .trim(), + ).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); + }); + }); +}); diff --git a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js new file mode 100644 index 00000000000..b323e1d8a06 --- /dev/null +++ b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js @@ -0,0 +1,84 @@ +import { shallowMount } from '@vue/test-utils'; +import JobGroupDropdown from '~/pipelines/components/graph/job_group_dropdown.vue'; + +describe('job group dropdown component', () => { + const group = { + jobs: [ + { + id: 4256, + name: '', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + tooltip: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4256', + has_details: true, + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4256/retry', + method: 'post', + }, + }, + }, + { + id: 4299, + name: 'test', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + tooltip: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4299', + has_details: true, + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4299/retry', + method: 'post', + }, + }, + }, + ], + name: 'rspec:linux', + size: 2, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + tooltip: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4256', + has_details: true, + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4256/retry', + method: 'post', + }, + }, + }; + + let wrapper; + const findButton = () => wrapper.find('button'); + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + wrapper = shallowMount(JobGroupDropdown, { propsData: { group } }); + }); + + it('renders button with group name and size', () => { + expect(findButton().text()).toContain(group.name); + expect(findButton().text()).toContain(group.size); + }); + + it('renders dropdown with jobs', () => { + expect(wrapper.findAll('.scrollable-menu>ul>li').length).toBe(group.jobs.length); + }); +}); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 0c64d5c9fa8..da777466e3e 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -47,7 +47,7 @@ describe('pipeline graph job item', () => { expect(link.attributes('title')).toEqual(`${mockJob.name} - ${mockJob.status.label}`); - expect(wrapper.find('.js-status-icon-success')).toBeDefined(); + expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(trimText(wrapper.find('.ci-status-text').text())).toBe(mockJob.name); @@ -73,7 +73,7 @@ describe('pipeline graph job item', () => { }, }); - expect(wrapper.find('.js-status-icon-success')).toBeDefined(); + expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(wrapper.find('a').exists()).toBe(false); expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name); @@ -84,8 +84,8 @@ describe('pipeline graph job item', () => { it('it should render the action icon', () => { createWrapper({ job: mockJob }); - expect(wrapper.find('a.ci-action-icon-container')).toBeDefined(); - expect(wrapper.find('i.ci-action-icon-wrapper')).toBeDefined(); + expect(wrapper.find('.ci-action-icon-container').exists()).toBe(true); + expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); }); }); diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js new file mode 100644 index 00000000000..3574b66403e --- /dev/null +++ b/spec/frontend/pipelines/graph/job_name_component_spec.js @@ -0,0 +1,36 @@ +import { mount } from '@vue/test-utils'; +import ciIcon from '~/vue_shared/components/ci_icon.vue'; + +import jobNameComponent from '~/pipelines/components/graph/job_name_component.vue'; + +describe('job name component', () => { + let wrapper; + + const propsData = { + name: 'foo', + status: { + icon: 'status_success', + group: 'success', + }, + }; + + beforeEach(() => { + wrapper = mount(jobNameComponent, { + propsData, + }); + }); + + it('should render the provided name', () => { + expect( + wrapper + .find('.ci-status-text') + .text() + .trim(), + ).toBe(propsData.name); + }); + + it('should render an icon with the provided status', () => { + expect(wrapper.find(ciIcon).exists()).toBe(true); + expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index 7f49b21100d..cf78aa3ef71 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -1,12 +1,17 @@ import { mount } from '@vue/test-utils'; 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; + describe('Linked pipeline', () => { let wrapper; + const findButton = () => wrapper.find('button'); const createWrapper = propsData => { wrapper = mount(LinkedPipelineComponent, { @@ -21,7 +26,7 @@ describe('Linked pipeline', () => { describe('rendered output', () => { const props = { pipeline: mockPipeline, - projectId: 20, + projectId: invalidTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -44,14 +49,13 @@ describe('Linked pipeline', () => { }); it('should render an svg within the status container', () => { - const pipelineStatusElement = wrapper.find('.js-linked-pipeline-status'); + const pipelineStatusElement = wrapper.find(CiStatus); expect(pipelineStatusElement.find('svg').exists()).toBe(true); }); it('should render the pipeline status icon svg', () => { - expect(wrapper.find('.js-ci-status-icon-running').exists()).toBe(true); - expect(wrapper.find('.js-ci-status-icon-running').html()).toContain(' { @@ -88,7 +92,7 @@ describe('Linked pipeline', () => { describe('parent/child', () => { const downstreamProps = { pipeline: mockPipeline, - projectId: 19, + projectId: validTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -116,7 +120,7 @@ describe('Linked pipeline', () => { describe('when isLoading is true', () => { const props = { pipeline: { ...mockPipeline, isLoading: true }, - projectId: 19, + projectId: invalidTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -132,7 +136,7 @@ describe('Linked pipeline', () => { describe('on click', () => { const props = { pipeline: mockPipeline, - projectId: 19, + projectId: validTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -142,18 +146,18 @@ describe('Linked pipeline', () => { it('emits `pipelineClicked` event', () => { jest.spyOn(wrapper.vm, '$emit'); - wrapper.find('button').trigger('click'); + findButton().trigger('click'); expect(wrapper.emitted().pipelineClicked).toBeTruthy(); }); it('should emit `bv::hide::tooltip` to close the tooltip', () => { jest.spyOn(wrapper.vm.$root, '$emit'); - wrapper.find('button').trigger('click'); + findButton().trigger('click'); expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([ 'bv::hide::tooltip', - 'js-linked-pipeline-132', + 'js-linked-pipeline-34993051', ]); }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js new file mode 100644 index 00000000000..82eaa553d0c --- /dev/null +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -0,0 +1,38 @@ +import { shallowMount } from '@vue/test-utils'; +import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; +import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue'; +import mockData from './linked_pipelines_mock_data'; + +describe('Linked Pipelines Column', () => { + const propsData = { + columnTitle: 'Upstream', + linkedPipelines: mockData.triggered, + graphPosition: 'right', + projectId: 19, + }; + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(LinkedPipelinesColumn, { propsData }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the pipeline orientation', () => { + const titleElement = wrapper.find('.linked-pipelines-column-title'); + + expect(titleElement.text()).toBe(propsData.columnTitle); + }); + + it('renders the correct number of linked pipelines', () => { + const linkedPipelineElements = wrapper.findAll(LinkedPipeline); + + expect(linkedPipelineElements.length).toBe(propsData.linkedPipelines.length); + }); + + it('renders cross project triangle when column is upstream', () => { + expect(wrapper.find('.cross-project-triangle').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js index c9a94b3101f..3e9c0814403 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js @@ -1,411 +1,3779 @@ export default { - project: { - id: 19, + 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', }, - triggered_by: { - id: 129, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/129', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/129', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: '7-5-stable', - path: '/gitlab-org/gitlab-foss/commits/7-5-stable', - tag: false, - branch: true, - }, - commit: { - id: '23433d4d8b20d7e45c103d0b6048faad38a130ab', - short_id: '23433d4d', - title: 'Version 7.5.0.rc1', - created_at: '2014-11-17T15:44:14.000+01:00', - parent_ids: ['30ac909f30f58d319b42ed1537664483894b18cd'], - message: 'Version 7.5.0.rc1\n', - author_name: 'Jacob Vosmaer', - author_email: 'contact@jacobvosmaer.nl', - authored_date: '2014-11-17T15:44:14.000+01:00', - committer_name: 'Jacob Vosmaer', - committer_email: 'contact@jacobvosmaer.nl', - committed_date: '2014-11-17T15:44:14.000+01:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', - commit_path: '/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', - }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/129/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/129/cancel', - created_at: '2017-05-24T14:46:20.090Z', - updated_at: '2017-05-24T14:46:29.906Z', + active: false, + coverage: null, + source: 'push', + 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, }, - triggered: [ - { - id: 132, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/132', - project: { - name: 'GitLabCE', - id: 19, - }, - details: { + 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_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/132', + details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#prebuild', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + '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', }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - short_id: 'b9d58c4c', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-03T12:50:33.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-03T12:50:33.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/132/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/132/cancel', - created_at: '2017-05-24T14:46:24.644Z', - updated_at: '2017-05-24T14:48:55.226Z', - }, - { - id: 133, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/133', - project: { - name: 'GitLabCE', - }, - details: { + { + 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_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/133', + details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#test', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + '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', }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', - short_id: 'b6bd4856', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-02T20:39:29.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-02T20:39:29.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/133/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/133/cancel', - created_at: '2017-05-24T14:46:24.648Z', - updated_at: '2017-05-24T14:48:59.673Z', - }, - { - id: 130, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/130', - project: { - name: 'GitLabCE', - }, - details: { + { + 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_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/130', + details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#cleanup', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + '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', }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, + ], + artifacts: [], + manual_actions: [ + { + name: 'review-docs-cleanup', + path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play', + playable: true, + scheduled: false, }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, + { + name: 'review-docs-deploy', + path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play', + playable: true, + scheduled: false, }, - commit: { - id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', - short_id: '6d7ced4a', - title: 'Whitespace fixes to patch', - created_at: '2013-10-08T13:53:22.000-05:00', - parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], - message: 'Whitespace fixes to patch\n', - author_name: 'Dale Hamel', - author_email: 'dale.hamel@srvthe.net', - authored_date: '2013-10-08T13:53:22.000-05:00', - committer_name: 'Dale Hamel', - committer_email: 'dale.hamel@invenia.ca', - committed_date: '2013-10-08T13:53:22.000-05:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + ], + 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', + 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', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/130/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/130/cancel', - created_at: '2017-05-24T14:46:24.630Z', - updated_at: '2017-05-24T14:49:45.091Z', + 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: [], }, - { - id: 131, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/132', - project: { - name: 'GitLabCE', + 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', + path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/132', + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + '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: [], }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - short_id: 'b9d58c4c', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-03T12:50:33.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-03T12:50:33.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + project: { + id: 20, + name: 'GitLab Docs', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/132/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/132/cancel', - created_at: '2017-05-24T14:46:24.644Z', - updated_at: '2017-05-24T14:48:55.226Z', }, + triggered: [], + }, + triggered: [ { - id: 134, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/133', - project: { - name: 'GitLabCE', + 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', + path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/133', + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + '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: [], }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', - short_id: 'b6bd4856', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-02T20:39:29.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-02T20:39:29.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', + project: { + id: 20, + name: 'GitLab Docs', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/133/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/133/cancel', - created_at: '2017-05-24T14:46:24.648Z', - updated_at: '2017-05-24T14:48:59.673Z', }, { - id: 135, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/130', - project: { - name: 'GitLabCE', + 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', + path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/130', + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + '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: [], }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', - short_id: '6d7ced4a', - title: 'Whitespace fixes to patch', - created_at: '2013-10-08T13:53:22.000-05:00', - parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], - message: 'Whitespace fixes to patch\n', - author_name: 'Dale Hamel', - author_email: 'dale.hamel@srvthe.net', - authored_date: '2013-10-08T13:53:22.000-05:00', - committer_name: 'Dale Hamel', - committer_email: 'dale.hamel@invenia.ca', - committed_date: '2013-10-08T13:53:22.000-05:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + project: { + id: 20, + name: 'GitLab Docs', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/130/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/130/cancel', - created_at: '2017-05-24T14:46:24.630Z', - updated_at: '2017-05-24T14:49:45.091Z', + triggered: [ + { + id: 26, + user: null, + active: false, + coverage: null, + source: 'push', + 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: 'master', + path: '/h5bp/html5-boilerplate/commits/master', + 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', + 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', + }, + }, + ], }, ], }; diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js new file mode 100644 index 00000000000..a4a5d78f906 --- /dev/null +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -0,0 +1,261 @@ +export default { + id: 123, + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + }, + active: false, + coverage: null, + path: '/root/ci-mock/pipelines/123', + details: { + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + }, + duration: 9, + finished_at: '2017-04-19T14:30:27.542Z', + stages: [ + { + name: 'test', + title: 'test: passed', + groups: [ + { + name: 'test', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4153', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4153/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4153, + name: 'test', + build_path: '/root/ci-mock/builds/4153', + retry_path: '/root/ci-mock/builds/4153/retry', + playable: false, + created_at: '2017-04-13T09:25:18.959Z', + updated_at: '2017-04-13T09:25:23.118Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4153', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4153/retry', + method: 'post', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123#test', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + }, + path: '/root/ci-mock/pipelines/123#test', + dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test', + }, + { + name: 'deploy ', + title: 'deploy: passed', + groups: [ + { + name: 'deploy to production', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4166', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4166/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4166, + name: 'deploy to production', + build_path: '/root/ci-mock/builds/4166', + retry_path: '/root/ci-mock/builds/4166/retry', + playable: false, + created_at: '2017-04-19T14:29:46.463Z', + updated_at: '2017-04-19T14:30:27.498Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4166', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4166/retry', + method: 'post', + }, + }, + }, + ], + }, + { + name: 'deploy to staging', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4159', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4159/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4159, + name: 'deploy to staging', + build_path: '/root/ci-mock/builds/4159', + retry_path: '/root/ci-mock/builds/4159/retry', + playable: false, + created_at: '2017-04-18T16:32:08.420Z', + updated_at: '2017-04-18T16:32:12.631Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4159', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4159/retry', + method: 'post', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123#deploy', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + }, + path: '/root/ci-mock/pipelines/123#deploy', + dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy', + }, + ], + artifacts: [], + manual_actions: [ + { + name: 'deploy to production', + path: '/root/ci-mock/builds/4166/play', + playable: false, + }, + ], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: false, + cancelable: false, + }, + ref: { + name: 'master', + path: '/root/ci-mock/tree/master', + tag: false, + branch: true, + }, + commit: { + id: '798e5f902592192afaba73f4668ae30e56eae492', + short_id: '798e5f90', + title: "Merge branch 'new-branch' into 'master'\r", + created_at: '2017-04-13T10:25:17.000+01:00', + parent_ids: [ + '54d483b1ed156fbbf618886ddf7ab023e24f8738', + 'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc', + ], + message: + "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1", + author_name: 'Root', + author_email: 'admin@example.com', + authored_date: '2017-04-13T10:25:17.000+01:00', + committer_name: 'Root', + committer_email: 'admin@example.com', + committed_date: '2017-04-13T10:25:17.000+01:00', + author: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: null, + commit_url: + 'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492', + commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492', + }, + created_at: '2017-04-13T09:25:18.881Z', + updated_at: '2017-04-19T14:30:27.561Z', +}; diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js new file mode 100644 index 00000000000..88e56eee1d6 --- /dev/null +++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js @@ -0,0 +1,136 @@ +import { shallowMount } from '@vue/test-utils'; + +import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; + +describe('stage column component', () => { + const mockJob = { + id: 4250, + name: 'test', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4250', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4250/retry', + method: 'post', + }, + }, + }; + + let wrapper; + + beforeEach(() => { + const mockGroups = []; + for (let i = 0; i < 3; i += 1) { + const mockedJob = Object.assign({}, mockJob); + mockedJob.id += i; + mockGroups.push(mockedJob); + } + + wrapper = shallowMount(stageColumnComponent, { + propsData: { + title: 'foo', + groups: mockGroups, + hasTriggeredBy: false, + }, + }); + }); + + it('should render provided title', () => { + expect( + wrapper + .find('.stage-name') + .text() + .trim(), + ).toBe('foo'); + }); + + it('should render the provided groups', () => { + expect(wrapper.findAll('.builds-container > ul > li').length).toBe( + wrapper.props('groups').length, + ); + }); + + describe('jobId', () => { + it('escapes job name', () => { + wrapper = shallowMount(stageColumnComponent, { + propsData: { + groups: [ + { + id: 4259, + name: '', + status: { + icon: 'status_success', + label: 'success', + tooltip: '', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + }, + }); + + expect(wrapper.find('.builds-container li').attributes('id')).toBe( + 'ci-badge-<img src=x onerror=alert(document.domain)>', + ); + }); + }); + + describe('with action', () => { + it('renders action button', () => { + wrapper = shallowMount(stageColumnComponent, { + propsData: { + groups: [ + { + id: 4259, + name: '', + status: { + icon: 'status_success', + label: 'success', + tooltip: '', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + action: { + icon: 'play', + title: 'Play all', + path: 'action', + }, + }, + }); + + expect(wrapper.find('.js-stage-action').exists()).toBe(true); + }); + }); + + describe('without action', () => { + it('does not render action button', () => { + wrapper = shallowMount(stageColumnComponent, { + propsData: { + groups: [ + { + id: 4259, + name: '', + status: { + icon: 'status_success', + label: 'success', + tooltip: '', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + }, + }); + + expect(wrapper.find('.js-stage-action').exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js index 3e46a29f776..f69b849521d 100644 --- a/spec/frontend/registry/explorer/pages/list_spec.js +++ b/spec/frontend/registry/explorer/pages/list_spec.js @@ -1,11 +1,12 @@ import VueRouter from 'vue-router'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; +import { GlPagination, GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui'; import Tracking from '~/tracking'; import component from '~/registry/explorer/pages/list.vue'; import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue'; import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue'; import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; +import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue'; import store from '~/registry/explorer/stores/'; import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { @@ -35,6 +36,8 @@ describe('List Page', () => { const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown); const findProjectEmptyState = () => wrapper.find(ProjectEmptyState); const findGroupEmptyState = () => wrapper.find(GroupEmptyState); + const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert); + const findDeleteAlert = () => wrapper.find(GlAlert); beforeEach(() => { wrapper = shallowMount(component, { @@ -57,6 +60,18 @@ describe('List Page', () => { wrapper.destroy(); }); + describe('Expiration policy notification', () => { + it('shows up on project page', () => { + expect(findProjectPolicyAlert().exists()).toBe(true); + }); + it('does show up on group page', () => { + store.dispatch('setInitialState', { isGroupPage: true }); + return wrapper.vm.$nextTick().then(() => { + expect(findProjectPolicyAlert().exists()).toBe(false); + }); + }); + }); + describe('connection error', () => { const config = { characterError: true, @@ -179,32 +194,38 @@ describe('List Page', () => { it('should call deleteItem when confirming deletion', () => { dispatchSpy.mockResolvedValue(); - const itemToDelete = wrapper.vm.images[0]; - wrapper.setData({ itemToDelete }); + findDeleteBtn().vm.$emit('click'); + expect(wrapper.vm.itemToDelete).not.toEqual({}); findDeleteModal().vm.$emit('ok'); expect(store.dispatch).toHaveBeenCalledWith( 'requestDeleteImage', - itemToDelete.destroy_path, + wrapper.vm.itemToDelete, ); }); - it('should show a success toast when delete request is successful', () => { + it('should show a success alert when delete request is successful', () => { dispatchSpy.mockResolvedValue(); + findDeleteBtn().vm.$emit('click'); + expect(wrapper.vm.itemToDelete).not.toEqual({}); return wrapper.vm.handleDeleteImage().then(() => { - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_IMAGE_SUCCESS_MESSAGE, { - type: 'success', - }); - expect(wrapper.vm.itemToDelete).toEqual({}); + const alert = findDeleteAlert(); + expect(alert.exists()).toBe(true); + expect(alert.text().replace(/\s\s+/gm, ' ')).toBe( + DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path), + ); }); }); - it('should show a error toast when delete request fails', () => { + it('should show an error alert when delete request fails', () => { dispatchSpy.mockRejectedValue(); + findDeleteBtn().vm.$emit('click'); + expect(wrapper.vm.itemToDelete).not.toEqual({}); return wrapper.vm.handleDeleteImage().then(() => { - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_IMAGE_ERROR_MESSAGE, { - type: 'error', - }); - expect(wrapper.vm.itemToDelete).toEqual({}); + const alert = findDeleteAlert(); + expect(alert.exists()).toBe(true); + expect(alert.text().replace(/\s\s+/gm, ' ')).toBe( + DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path), + ); }); }); }); diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js index b39c79dd1ab..58f61a0e8c2 100644 --- a/spec/frontend/registry/explorer/stores/actions_spec.js +++ b/spec/frontend/registry/explorer/stores/actions_spec.js @@ -279,39 +279,32 @@ describe('Actions RegistryExplorer Store', () => { }); describe('request delete single image', () => { - const deletePath = 'delete/path'; + const image = { + destroy_path: 'delete/path', + }; + it('successfully performs the delete request', done => { - mock.onDelete(deletePath).replyOnce(200); + mock.onDelete(image.destroy_path).replyOnce(200); testAction( actions.requestDeleteImage, - deletePath, - { - pagination: {}, - }, + image, + {}, [ { type: types.SET_MAIN_LOADING, payload: true }, + { type: types.UPDATE_IMAGE, payload: { ...image, deleting: true } }, { type: types.SET_MAIN_LOADING, payload: false }, ], - [ - { - type: 'setShowGarbageCollectionTip', - payload: true, - }, - { - type: 'requestImagesList', - payload: { pagination: {} }, - }, - ], + [], done, ); }); it('should turn off loading on error', done => { - mock.onDelete(deletePath).replyOnce(400); + mock.onDelete(image.destroy_path).replyOnce(400); testAction( actions.requestDeleteImage, - deletePath, + image, {}, [ { type: types.SET_MAIN_LOADING, payload: true }, diff --git a/spec/frontend/registry/explorer/stores/mutations_spec.js b/spec/frontend/registry/explorer/stores/mutations_spec.js index 029fd23f7ce..43b2ba84218 100644 --- a/spec/frontend/registry/explorer/stores/mutations_spec.js +++ b/spec/frontend/registry/explorer/stores/mutations_spec.js @@ -28,14 +28,32 @@ describe('Mutations Registry Explorer Store', () => { describe('SET_IMAGES_LIST_SUCCESS', () => { it('should set the images list', () => { - const images = [1, 2, 3]; - const expectedState = { ...mockState, images }; + const images = [{ name: 'foo' }, { name: 'bar' }]; + const defaultStatus = { deleting: false, failedDelete: false }; + const expectedState = { + ...mockState, + images: [{ name: 'foo', ...defaultStatus }, { name: 'bar', ...defaultStatus }], + }; mutations[types.SET_IMAGES_LIST_SUCCESS](mockState, images); expect(mockState).toEqual(expectedState); }); }); + describe('UPDATE_IMAGE', () => { + it('should update an image', () => { + mockState.images = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]; + const payload = { id: 1, name: 'baz' }; + const expectedState = { + ...mockState, + images: [payload, { id: 2, name: 'bar' }], + }; + mutations[types.UPDATE_IMAGE](mockState, payload); + + expect(mockState).toEqual(expectedState); + }); + }); + describe('SET_TAGS_LIST_SUCCESS', () => { it('should set the tags list', () => { const tags = [1, 2, 3]; diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index 6944b23558a..8f3ac53c37a 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -4,15 +4,14 @@ import createRouter from '~/repository/router'; describe('Repository router spec', () => { it.each` - path | branch | component | componentName - ${'/'} | ${'master'} | ${IndexPage} | ${'IndexPage'} - ${'/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/master/app/assets'} | ${'master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/feature/test-%23/app/assets'} | ${'feature/test-#'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/123/app/assets'} | ${'master'} | ${null} | ${'null'} - `('sets component as $componentName for path "$path"', ({ path, component, branch }) => { - const router = createRouter('', branch); + path | component | componentName + ${'/'} | ${IndexPage} | ${'IndexPage'} + ${'/tree/master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/master/app/assets'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/123/app/assets'} | ${null} | ${'null'} + `('sets component as $componentName for path "$path"', ({ path, component }) => { + const router = createRouter('', 'master'); const componentsForRoute = router.getMatchedComponents(path); diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js new file mode 100644 index 00000000000..c1876066a21 --- /dev/null +++ b/spec/frontend/sidebar/sidebar_assignees_spec.js @@ -0,0 +1,74 @@ +import { shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; +import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue'; +import Assigness from '~/sidebar/components/assignees/assignees.vue'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; +import SidebarService from '~/sidebar/services/sidebar_service'; +import SidebarStore from '~/sidebar/stores/sidebar_store'; +import Mock from './mock_data'; + +describe('sidebar assignees', () => { + let wrapper; + let mediator; + let axiosMock; + + const createComponent = () => { + wrapper = shallowMount(SidebarAssignees, { + propsData: { + mediator, + field: '', + }, + // Attaching to document is required because this component emits something from the parent element :/ + attachToDocument: true, + }); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + mediator = new SidebarMediator(Mock.mediator); + + jest.spyOn(mediator, 'saveAssignees'); + jest.spyOn(mediator, 'assignYourself'); + + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + SidebarService.singleton = null; + SidebarStore.singleton = null; + SidebarMediator.singleton = null; + axiosMock.restore(); + }); + + it('calls the mediator when saves the assignees', () => { + expect(mediator.saveAssignees).not.toHaveBeenCalled(); + + wrapper.vm.saveAssignees(); + + expect(mediator.saveAssignees).toHaveBeenCalled(); + }); + + it('calls the mediator when "assignSelf" method is called', () => { + expect(mediator.assignYourself).not.toHaveBeenCalled(); + expect(mediator.store.assignees.length).toBe(0); + + wrapper.vm.assignSelf(); + + expect(mediator.assignYourself).toHaveBeenCalled(); + expect(mediator.store.assignees.length).toBe(1); + }); + + it('hides assignees until fetched', () => { + expect(wrapper.find(Assigness).exists()).toBe(false); + + wrapper.vm.store.isFetching.assignees = false; + + return wrapper.vm.$nextTick(() => { + expect(wrapper.find(Assigness).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/snippet/snippet_edit_spec.js b/spec/frontend/snippet/snippet_edit_spec.js new file mode 100644 index 00000000000..cfe5062c86b --- /dev/null +++ b/spec/frontend/snippet/snippet_edit_spec.js @@ -0,0 +1,45 @@ +import '~/snippet/snippet_edit'; +import { SnippetEditInit } from '~/snippets'; +import initSnippet from '~/snippet/snippet_bundle'; + +import { triggerDOMEvent } from 'jest/helpers/dom_events_helper'; + +jest.mock('~/snippet/snippet_bundle'); +jest.mock('~/snippets'); + +describe('Snippet edit form initialization', () => { + const setFF = flag => { + gon.features = { snippetsEditVue: flag }; + }; + let features; + + beforeEach(() => { + features = gon.features; + setFixtures('
'); + }); + + afterEach(() => { + gon.features = features; + }); + + it.each` + name | flag | isVue + ${'Regular'} | ${false} | ${false} + ${'Vue'} | ${true} | ${true} + `('correctly initializes $name Snippet Edit form', ({ flag, isVue }) => { + initSnippet.mockClear(); + SnippetEditInit.mockClear(); + + setFF(flag); + + triggerDOMEvent('DOMContentLoaded'); + + if (isVue) { + expect(initSnippet).not.toHaveBeenCalled(); + expect(SnippetEditInit).toHaveBeenCalled(); + } else { + expect(initSnippet).toHaveBeenCalled(); + expect(SnippetEditInit).not.toHaveBeenCalled(); + } + }); +}); 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 3c3f9764f64..334ceaa064f 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 @@ -39,7 +39,6 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = qa-description-textarea" data-supports-quick-actions="false" dir="auto" - id="snippet-description" placeholder="Write a comment or drag your files here…" /> diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js new file mode 100644 index 00000000000..21a4ccf5a74 --- /dev/null +++ b/spec/frontend/snippets/components/edit_spec.js @@ -0,0 +1,279 @@ +import { shallowMount } from '@vue/test-utils'; +import axios from '~/lib/utils/axios_utils'; + +import { GlLoadingIcon } from '@gitlab/ui'; +import { joinPaths, redirectTo } from '~/lib/utils/url_utility'; + +import SnippetEditApp from '~/snippets/components/edit.vue'; +import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue'; +import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; +import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; +import TitleField from '~/vue_shared/components/form/title.vue'; +import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; + +import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql'; +import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql'; + +import AxiosMockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import { ApolloMutation } from 'vue-apollo'; + +jest.mock('~/lib/utils/url_utility', () => ({ + getBaseURL: jest.fn().mockReturnValue('foo/'), + redirectTo: jest.fn().mockName('redirectTo'), + joinPaths: jest + .fn() + .mockName('joinPaths') + .mockReturnValue('contentApiURL'), +})); + +let flashSpy; + +const contentMock = 'Foo Bar'; +const rawPathMock = '/foo/bar'; +const rawProjectPathMock = '/project/path'; +const newlyEditedSnippetUrl = 'http://foo.bar'; +const apiError = { message: 'Ufff' }; + +const defaultProps = { + snippetGid: 'gid://gitlab/PersonalSnippet/42', + markdownPreviewPath: 'http://preview.foo.bar', + markdownDocsPath: 'http://docs.foo.bar', +}; + +describe('Snippet Edit app', () => { + let wrapper; + let axiosMock; + + const resolveMutate = jest.fn().mockResolvedValue({ + data: { + updateSnippet: { + errors: [], + snippet: { + webUrl: newlyEditedSnippetUrl, + }, + }, + }, + }); + + const rejectMutation = jest.fn().mockRejectedValue(apiError); + + const mutationTypes = { + RESOLVE: resolveMutate, + REJECT: rejectMutation, + }; + + function createComponent({ + props = defaultProps, + data = {}, + loading = false, + mutationRes = mutationTypes.RESOLVE, + } = {}) { + const $apollo = { + queries: { + snippet: { + loading, + }, + }, + mutate: mutationRes, + }; + + wrapper = shallowMount(SnippetEditApp, { + mocks: { $apollo }, + stubs: { + FormFooterActions, + ApolloMutation, + }, + propsData: { + ...props, + }, + data() { + return data; + }, + }); + + flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure'); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findSubmitButton = () => wrapper.find('[type=submit]'); + + describe('rendering', () => { + it('renders loader while the query is in flight', () => { + createComponent({ loading: true }); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('renders all required components', () => { + createComponent(); + + expect(wrapper.contains(TitleField)).toBe(true); + expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true); + expect(wrapper.contains(SnippetBlobEdit)).toBe(true); + expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true); + expect(wrapper.contains(FormFooterActions)).toBe(true); + }); + + it('does not fail if there is no snippet yet (new snippet creation)', () => { + const snippetGid = ''; + createComponent({ + props: { + ...defaultProps, + snippetGid, + }, + }); + + expect(wrapper.props('snippetGid')).toBe(snippetGid); + }); + + it.each` + title | content | expectation + ${''} | ${''} | ${true} + ${'foo'} | ${''} | ${true} + ${''} | ${'foo'} | ${true} + ${'foo'} | ${'bar'} | ${false} + `( + 'disables submit button unless both title and content are present', + ({ title, content, expectation }) => { + createComponent({ + data: { + snippet: { title }, + content, + }, + }); + const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled')); + expect(isBtnDisabled).toBe(expectation); + }, + ); + }); + + describe('functionality', () => { + describe('handling of the data from GraphQL response', () => { + const snippet = { + blob: { + rawPath: rawPathMock, + }, + }; + const getResSchema = newSnippet => { + return { + data: { + snippets: { + edges: newSnippet ? [] : [snippet], + }, + }, + }; + }; + + const bootstrapForExistingSnippet = resp => { + createComponent({ + data: { + snippet, + }, + }); + + if (resp === 500) { + axiosMock.onGet('contentApiURL').reply(500); + } else { + axiosMock.onGet('contentApiURL').reply(200, contentMock); + } + wrapper.vm.onSnippetFetch(getResSchema()); + }; + + const bootstrapForNewSnippet = () => { + createComponent(); + wrapper.vm.onSnippetFetch(getResSchema(true)); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + + it('fetches blob content with the additional query', () => { + bootstrapForExistingSnippet(); + + return waitForPromises().then(() => { + expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock); + expect(wrapper.vm.newSnippet).toBe(false); + expect(wrapper.vm.content).toBe(contentMock); + }); + }); + + it('flashes the error message if fetching content fails', () => { + bootstrapForExistingSnippet(500); + + return waitForPromises().then(() => { + expect(flashSpy).toHaveBeenCalled(); + expect(wrapper.vm.content).toBe(''); + }); + }); + + it('does not fetch content for new snippet', () => { + bootstrapForNewSnippet(); + + return waitForPromises().then(() => { + // we keep using waitForPromises to make sure we do not run failed test + expect(wrapper.vm.newSnippet).toBe(true); + expect(wrapper.vm.content).toBe(''); + expect(joinPaths).not.toHaveBeenCalled(); + expect(wrapper.vm.snippet).toEqual(wrapper.vm.$options.newSnippetSchema); + }); + }); + }); + + describe('form submission handling', () => { + it.each` + newSnippet | projectPath | mutation | mutationName + ${true} | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'} + ${true} | ${''} | ${CreateSnippetMutation} | ${'CreateSnippetMutation without projectPath'} + ${false} | ${rawProjectPathMock} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation with projectPath'} + ${false} | ${''} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation without projectPath'} + `('should submit $mutationName correctly', ({ newSnippet, projectPath, mutation }) => { + createComponent({ + data: { + newSnippet, + }, + props: { + ...defaultProps, + projectPath, + }, + }); + + const mutationPayload = { + mutation, + variables: { + input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object), + }, + }; + + wrapper.vm.handleFormSubmit(); + expect(resolveMutate).toHaveBeenCalledWith(mutationPayload); + }); + + it('redirects to snippet view on successful mutation', () => { + createComponent(); + wrapper.vm.handleFormSubmit(); + return waitForPromises().then(() => { + expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl); + }); + }); + + it('flashes an error if mutation failed', () => { + createComponent({ + mutationRes: mutationTypes.REJECT, + }); + wrapper.vm.handleFormSubmit(); + return waitForPromises().then(() => { + expect(redirectTo).not.toHaveBeenCalled(); + expect(flashSpy).toHaveBeenCalledWith(apiError); + }); + }); + }); + }); +}); diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index 1b67c08e5a4..16a66c70d6a 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -1,7 +1,7 @@ import SnippetHeader from '~/snippets/components/snippet_header.vue'; import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql'; import { ApolloMutation } from 'vue-apollo'; -import { GlNewButton, GlModal } from '@gitlab/ui'; +import { GlButton, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; describe('Snippet header component', () => { @@ -89,7 +89,7 @@ describe('Snippet header component', () => { updateSnippet: false, }, }); - expect(wrapper.findAll(GlNewButton).length).toEqual(0); + expect(wrapper.findAll(GlButton).length).toEqual(0); createComponent({ permissions: { @@ -97,7 +97,7 @@ describe('Snippet header component', () => { updateSnippet: false, }, }); - expect(wrapper.findAll(GlNewButton).length).toEqual(1); + expect(wrapper.findAll(GlButton).length).toEqual(1); createComponent({ permissions: { @@ -105,7 +105,7 @@ describe('Snippet header component', () => { updateSnippet: true, }, }); - expect(wrapper.findAll(GlNewButton).length).toEqual(2); + expect(wrapper.findAll(GlButton).length).toEqual(2); createComponent({ permissions: { @@ -117,7 +117,7 @@ describe('Snippet header component', () => { canCreateSnippet: true, }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.findAll(GlNewButton).length).toEqual(3); + expect(wrapper.findAll(GlButton).length).toEqual(3); }); }); diff --git a/spec/frontend/static_site_editor/components/invalid_content_message_spec.js b/spec/frontend/static_site_editor/components/invalid_content_message_spec.js new file mode 100644 index 00000000000..7e699e9451c --- /dev/null +++ b/spec/frontend/static_site_editor/components/invalid_content_message_spec.js @@ -0,0 +1,23 @@ +import { shallowMount } from '@vue/test-utils'; + +import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; + +describe('~/static_site_editor/components/invalid_content_message.vue', () => { + let wrapper; + const findDocumentationButton = () => wrapper.find({ ref: 'documentationButton' }); + const documentationUrl = + 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman'; + + beforeEach(() => { + wrapper = shallowMount(InvalidContentMessage); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the configuration button link', () => { + expect(findDocumentationButton().exists()).toBe(true); + expect(findDocumentationButton().attributes('href')).toBe(documentationUrl); + }); +}); diff --git a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js index f00fc38430f..82eb12d4c4d 100644 --- a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js +++ b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlNewButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; @@ -18,7 +18,7 @@ describe('Static Site Editor Toolbar', () => { }; const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' }); - const findSaveChangesButton = () => wrapper.find(GlNewButton); + const findSaveChangesButton = () => wrapper.find(GlButton); const findLoadingIndicator = () => wrapper.find(GlLoadingIcon); beforeEach(() => { diff --git a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js b/spec/frontend/static_site_editor/components/saved_changes_message_spec.js index 76ac7de5c32..659e9be59d2 100644 --- a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js +++ b/spec/frontend/static_site_editor/components/saved_changes_message_spec.js @@ -1,22 +1,17 @@ import { shallowMount } from '@vue/test-utils'; + import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue'; +import { returnUrl, savedContentMeta } from '../mock_data'; + describe('~/static_site_editor/components/saved_changes_message.vue', () => { let wrapper; + const { branch, commit, mergeRequest } = savedContentMeta; const props = { - branch: { - label: '123-the-branch', - url: 'https://gitlab.com/gitlab-org/gitlab/-/tree/123-the-branch', - }, - commit: { - label: 'a123', - url: 'https://gitlab.com/gitlab-org/gitlab/-/commit/a123', - }, - mergeRequest: { - label: '123', - url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123', - }, - returnUrl: 'https://www.the-static-site.com/post', + branch, + commit, + mergeRequest, + returnUrl, }; const findReturnToSiteButton = () => wrapper.find({ ref: 'returnToSiteButton' }); const findMergeRequestButton = () => wrapper.find({ ref: 'mergeRequestButton' }); @@ -51,11 +46,14 @@ describe('~/static_site_editor/components/saved_changes_message.vue', () => { ${'branch'} | ${findBranchLink} | ${props.branch} ${'commit'} | ${findCommitLink} | ${props.commit} ${'merge request'} | ${findMergeRequestLink} | ${props.mergeRequest} - `('renders $desc link', ({ findEl, prop }) => { + `('renders $desc link', ({ desc, findEl, prop }) => { const el = findEl(); expect(el.exists()).toBe(true); - expect(el.attributes('href')).toBe(prop.url); expect(el.text()).toBe(prop.label); + + if (desc !== 'branch') { + expect(el.attributes('href')).toBe(prop.url); + } }); }); diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js index d427df9bd4b..5d4e3758557 100644 --- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js +++ b/spec/frontend/static_site_editor/components/static_site_editor_spec.js @@ -1,6 +1,5 @@ import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; - import { GlSkeletonLoader } from '@gitlab/ui'; import createState from '~/static_site_editor/store/state'; @@ -8,9 +7,18 @@ import createState from '~/static_site_editor/store/state'; import StaticSiteEditor from '~/static_site_editor/components/static_site_editor.vue'; import EditArea from '~/static_site_editor/components/edit_area.vue'; import EditHeader from '~/static_site_editor/components/edit_header.vue'; +import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; +import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue'; +import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue'; -import { sourceContent, sourceContentTitle } from '../mock_data'; +import { + returnUrl, + sourceContent, + sourceContentTitle, + savedContentMeta, + submitChangesError, +} from '../mock_data'; const localVue = createLocalVue(); @@ -22,14 +30,19 @@ describe('StaticSiteEditor', () => { let loadContentActionMock; let setContentActionMock; let submitChangesActionMock; + let dismissSubmitChangesErrorActionMock; const buildStore = ({ initialState, getters } = {}) => { loadContentActionMock = jest.fn(); setContentActionMock = jest.fn(); submitChangesActionMock = jest.fn(); + dismissSubmitChangesErrorActionMock = jest.fn(); store = new Vuex.Store({ - state: createState(initialState), + state: createState({ + isSupportedContent: true, + ...initialState, + }), getters: { contentChanged: () => false, ...getters, @@ -38,6 +51,7 @@ describe('StaticSiteEditor', () => { loadContent: loadContentActionMock, setContent: setContentActionMock, submitChanges: submitChangesActionMock, + dismissSubmitChangesError: dismissSubmitChangesErrorActionMock, }, }); }; @@ -62,8 +76,11 @@ describe('StaticSiteEditor', () => { const findEditArea = () => wrapper.find(EditArea); const findEditHeader = () => wrapper.find(EditHeader); + const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage); const findPublishToolbar = () => wrapper.find(PublishToolbar); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); + const findSubmitChangesError = () => wrapper.find(SubmitChangesError); + const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage); beforeEach(() => { buildStore(); @@ -74,6 +91,17 @@ describe('StaticSiteEditor', () => { wrapper.destroy(); }); + it('renders the saved changes message when changes are submitted successfully', () => { + buildStore({ initialState: { returnUrl, savedContentMeta } }); + buildWrapper(); + + expect(findSavedChangesMessage().exists()).toBe(true); + expect(findSavedChangesMessage().props()).toEqual({ + returnUrl, + ...savedContentMeta, + }); + }); + describe('when content is not loaded', () => { it('does not render edit area', () => { expect(findEditArea().exists()).toBe(false); @@ -86,6 +114,10 @@ describe('StaticSiteEditor', () => { it('does not render toolbar', () => { expect(findPublishToolbar().exists()).toBe(false); }); + + it('does not render saved changes message', () => { + expect(findSavedChangesMessage().exists()).toBe(false); + }); }); describe('when content is loaded', () => { @@ -140,6 +172,13 @@ describe('StaticSiteEditor', () => { expect(findSkeletonLoader().exists()).toBe(true); }); + it('does not display submit changes error when an error does not exist', () => { + buildContentLoadedStore(); + buildWrapper(); + + expect(findSubmitChangesError().exists()).toBe(false); + }); + it('sets toolbar as saving when saving changes', () => { buildContentLoadedStore({ initialState: { @@ -151,6 +190,40 @@ describe('StaticSiteEditor', () => { expect(findPublishToolbar().props('savingChanges')).toBe(true); }); + it('displays invalid content message when content is not supported', () => { + buildStore({ initialState: { isSupportedContent: false } }); + buildWrapper(); + + expect(findInvalidContentMessage().exists()).toBe(true); + }); + + describe('when submitting changes fail', () => { + beforeEach(() => { + buildContentLoadedStore({ + initialState: { + submitChangesError, + }, + }); + buildWrapper(); + }); + + it('displays submit changes error message', () => { + expect(findSubmitChangesError().exists()).toBe(true); + }); + + it('dispatches submitChanges action when error message emits retry event', () => { + findSubmitChangesError().vm.$emit('retry'); + + expect(submitChangesActionMock).toHaveBeenCalled(); + }); + + it('dispatches dismissSubmitChangesError action when error message emits dismiss event', () => { + findSubmitChangesError().vm.$emit('dismiss'); + + expect(dismissSubmitChangesErrorActionMock).toHaveBeenCalled(); + }); + }); + it('dispatches load content action', () => { expect(loadContentActionMock).toHaveBeenCalled(); }); diff --git a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js new file mode 100644 index 00000000000..7af3014b338 --- /dev/null +++ b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js @@ -0,0 +1,48 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton, GlAlert } from '@gitlab/ui'; + +import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue'; + +import { submitChangesError as error } from '../mock_data'; + +describe('Submit Changes Error', () => { + let wrapper; + + const buildWrapper = (propsData = {}) => { + wrapper = shallowMount(SubmitChangesError, { + propsData: { + ...propsData, + }, + stubs: { + GlAlert, + }, + }); + }; + + const findRetryButton = () => wrapper.find(GlButton); + const findAlert = () => wrapper.find(GlAlert); + + beforeEach(() => { + buildWrapper({ error }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders error message', () => { + expect(findAlert().text()).toContain(error); + }); + + it('emits dismiss event when alert emits dismiss event', () => { + findAlert().vm.$emit('dismiss'); + + expect(wrapper.emitted('dismiss')).toHaveLength(1); + }); + + it('emits retry event when retry button is clicked', () => { + findRetryButton().vm.$emit('click'); + + expect(wrapper.emitted('retry')).toHaveLength(1); + }); +}); diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js index 345ae0ce6f6..962047e6dd2 100644 --- a/spec/frontend/static_site_editor/mock_data.js +++ b/spec/frontend/static_site_editor/mock_data.js @@ -21,10 +21,10 @@ export const sourcePath = 'foobar.md.html'; export const savedContentMeta = { branch: { label: 'foobar', - url: 'foobar/-/tree/foorbar', + url: 'foobar/-/tree/foobar', }, commit: { - label: 'c1461b08 ', + label: 'c1461b08', url: 'foobar/-/c1461b08', }, mergeRequest: { diff --git a/spec/frontend/static_site_editor/store/actions_spec.js b/spec/frontend/static_site_editor/store/actions_spec.js index a9c039517b7..6b0b77f59b7 100644 --- a/spec/frontend/static_site_editor/store/actions_spec.js +++ b/spec/frontend/static_site_editor/store/actions_spec.js @@ -124,24 +124,29 @@ describe('Static Site Editor Store actions', () => { }); describe('on error', () => { + const error = new Error(submitChangesError); const expectedMutations = [ { type: mutationTypes.SUBMIT_CHANGES }, - { type: mutationTypes.SUBMIT_CHANGES_ERROR }, + { type: mutationTypes.SUBMIT_CHANGES_ERROR, payload: error.message }, ]; beforeEach(() => { - submitContentChanges.mockRejectedValueOnce(new Error(submitChangesError)); + submitContentChanges.mockRejectedValueOnce(error); }); it('dispatches receiveContentError', () => { testAction(actions.submitChanges, null, state, expectedMutations); }); + }); + }); - it('displays flash communicating error', () => { - return testAction(actions.submitChanges, null, state, expectedMutations).then(() => { - expect(createFlash).toHaveBeenCalledWith(submitChangesError); - }); - }); + describe('dismissSubmitChangesError', () => { + it('commits dismissSubmitChangesError', () => { + testAction(actions.dismissSubmitChangesError, null, state, [ + { + type: mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR, + }, + ]); }); }); }); diff --git a/spec/frontend/static_site_editor/store/mutations_spec.js b/spec/frontend/static_site_editor/store/mutations_spec.js index 0b213c11a04..2441f317d90 100644 --- a/spec/frontend/static_site_editor/store/mutations_spec.js +++ b/spec/frontend/static_site_editor/store/mutations_spec.js @@ -5,6 +5,7 @@ import { sourceContentTitle as title, sourceContent as content, savedContentMeta, + submitChangesError, } from '../mock_data'; describe('Static Site Editor Store mutations', () => { @@ -16,19 +17,21 @@ describe('Static Site Editor Store mutations', () => { }); it.each` - mutation | stateProperty | payload | expectedValue - ${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content} - ${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false} - ${types.SET_CONTENT} | ${'content'} | ${content} | ${content} - ${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true} - ${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta} - ${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false} - ${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false} + mutation | stateProperty | payload | expectedValue + ${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content} + ${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false} + ${types.SET_CONTENT} | ${'content'} | ${content} | ${content} + ${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true} + ${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta} + ${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false} + ${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false} + ${types.SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${submitChangesError} | ${submitChangesError} + ${types.DISMISS_SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${undefined} | ${''} `( '$mutation sets $stateProperty to $expectedValue', ({ mutation, stateProperty, payload, expectedValue }) => { diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap new file mode 100644 index 00000000000..df4b30f1cb8 --- /dev/null +++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap @@ -0,0 +1,287 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` +
+ + + + + + + + +
+