From b6927b3dd66fc3e5954f866c692b0ee8e276ec57 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 4 Mar 2022 03:19:33 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../components/commit/commit_form.vue | 1 + .../ui/pipeline_editor_empty_state.vue | 7 +- .../javascripts/pipeline_editor/constants.js | 44 + .../components/pipelines_list/empty_state.vue | 11 +- .../components/pipelines_list/pipelines.vue | 6 + .../pipelines_list/pipelines_ci_templates.vue | 230 ++-- .../javascripts/pipelines/pipelines_index.js | 2 + .../projects/pipelines_controller.rb | 13 + app/helpers/ci/pipelines_helper.rb | 31 + app/views/projects/pipelines/index.html.haml | 20 +- .../runners_availability_section.yml | 8 + .../configuration/index.md | 3 + .../vulnerabilities/index.md | 24 + lib/gitlab.rb | 2 +- locale/gitlab.pot | 31 +- qa/qa/page/merge_request/show.rb | 8 +- qa/qa/page/project/pipeline_editor/new.rb | 19 + qa/qa/page/project/pipeline_editor/show.rb | 19 +- ...ne_editor_can_create_merge_request_spec.rb | 53 + .../projects/pipelines_controller_spec.rb | 4 + .../features/projects/blobs/blob_show_spec.rb | 563 -------- .../projects/pipelines/pipelines_spec.rb | 2 +- .../projects/blobs/balsamiq_spec.rb | 18 + .../blobs/blob_line_permalink_updater_spec.rb | 103 ++ .../projects/blobs/blob_show_spec.rb | 1154 +++++++++++++++++ .../projects/blobs/edit_spec.rb | 213 +++ .../projects/blobs/shortcuts_blob_spec.rb | 45 + ...er_creates_new_blob_in_new_project_spec.rb | 63 + ...ser_follows_pipeline_suggest_nudge_spec.rb | 80 ++ .../user_views_pipeline_editor_button_spec.rb | 46 + .../projects/files/editing_a_file_spec.rb | 34 + .../projects/files/find_file_keyboard_spec.rb | 42 + ...project_owner_creates_license_file_spec.rb | 72 + .../projects/files/user_browses_files_spec.rb | 377 ++++++ .../files/user_browses_lfs_files_spec.rb | 86 ++ .../projects/files/user_deletes_files_spec.rb | 74 ++ .../projects/files/user_edits_files_spec.rb | 226 ++++ .../files/user_replaces_files_spec.rb | 93 ++ .../pipelines/pipelines_ci_templates_spec.js | 96 +- spec/helpers/ci/pipelines_helper_spec.rb | 59 + spec/lib/gitlab_spec.rb | 15 +- 41 files changed, 3313 insertions(+), 684 deletions(-) create mode 100644 config/feature_flags/experiment/runners_availability_section.yml create mode 100644 qa/qa/page/project/pipeline_editor/new.rb create mode 100644 qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/balsamiq_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_line_permalink_updater_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_show_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/edit_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/shortcuts_blob_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/user_creates_new_blob_in_new_project_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/blobs/user_views_pipeline_editor_button_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/editing_a_file_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/find_file_keyboard_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/project_owner_creates_license_file_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_files_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_lfs_files_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/user_deletes_files_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/user_edits_files_spec.rb create mode 100644 spec/features/refactor_blob_viewer_disabled/projects/files/user_replaces_files_spec.rb diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue index 107af6b4705..8536db78dfb 100644 --- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue +++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue @@ -136,6 +136,7 @@ export default { v-if="!isCurrentBranchTarget" v-model="openMergeRequest" data-testid="new-mr-checkbox" + data-qa-selector="new_mr_checkbox" class="gl-mt-3" > diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue index dcd08c9de8d..aee71999373 100644 --- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue +++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue @@ -41,7 +41,12 @@ export default {

- + {{ $options.i18n.btnText }} diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js index 1382b1d50eb..2ebc4306405 100644 --- a/app/assets/javascripts/pipeline_editor/constants.js +++ b/app/assets/javascripts/pipeline_editor/constants.js @@ -1,3 +1,5 @@ +import { s__ } from '~/locale'; + // Values for CI_CONFIG_STATUS_* comes from lint graphQL export const CI_CONFIG_STATUS_INVALID = 'INVALID'; export const CI_CONFIG_STATUS_VALID = 'VALID'; @@ -62,3 +64,45 @@ export const TEMPLATE_REPOSITORY_URL = 'https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates'; export const COMMIT_SHA_POLL_INTERVAL = 1000; + +export const RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME = 'runners_availability_section'; +export const RUNNERS_SETTINGS_LINK_CLICKED_EVENT = 'runners_settings_link_clicked'; +export const RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT = 'runners_documentation_link_clicked'; +export const RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT = 'runners_settings_button_clicked'; +export const I18N = { + title: s__('Pipelines|Get started with GitLab CI/CD'), + runners: { + title: s__('Pipelines|Runners are available to run your jobs now'), + subtitle: s__( + 'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners.', + ), + }, + noRunners: { + title: s__('Pipelines|No runners detected'), + subtitle: s__( + 'Pipelines|A GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. Install GitLab Runner and register your own runners to get started with CI/CD.', + ), + cta: s__('Pipelines|Install GitLab Runner'), + }, + learnBasics: { + title: s__('Pipelines|Learn the basics of pipelines and .yml files'), + subtitle: s__( + 'Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works.', + ), + gettingStarted: { + title: s__('Pipelines|"Hello world" with GitLab CI'), + description: s__( + 'Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a "Hello world" script to see how it runs, explore how CI/CD works.', + ), + cta: s__('Pipelines|Try test template'), + }, + }, + templates: { + title: s__('Pipelines|Ready to set up CI/CD for your project?'), + subtitle: s__( + "Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD.", + ), + description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'), + cta: s__('Pipelines|Use template'), + }, +}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue index 1ce6654e0e9..cd6f7427fd6 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue @@ -49,6 +49,11 @@ export default { required: false, default: null, }, + anyRunnersAvailable: { + type: Boolean, + required: false, + default: true, + }, }, computed: { ciHelpPagePath() { @@ -120,7 +125,11 @@ export default { - + -import { GlAvatar, GlButton, GlCard, GlSprintf } from '@gitlab/ui'; +import { GlAvatar, GlButton, GlCard, GlSprintf, GlIcon, GlLink } from '@gitlab/ui'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import { s__, sprintf } from '~/locale'; -import { STARTER_TEMPLATE_NAME } from '~/pipeline_editor/constants'; +import { sprintf } from '~/locale'; +import { + STARTER_TEMPLATE_NAME, + RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME, + RUNNERS_SETTINGS_LINK_CLICKED_EVENT, + RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT, + RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT, + I18N, +} from '~/pipeline_editor/constants'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; +import ExperimentTracking from '~/experimentation/experiment_tracking'; +import { getExperimentData } from '~/experimentation/utils'; import Tracking from '~/tracking'; export default { @@ -11,39 +22,37 @@ export default { GlButton, GlCard, GlSprintf, + GlIcon, + GlLink, + GitlabExperiment, }, mixins: [Tracking.mixin()], STARTER_TEMPLATE_NAME, - i18n: { - cta: s__('Pipelines|Use template'), - testTemplates: { - title: s__('Pipelines|Use a sample CI/CD template'), - subtitle: s__( - 'Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works.', - ), - gettingStarted: { - title: s__('Pipelines|Get started with GitLab CI/CD'), - description: s__( - 'Pipelines|Get familiar with GitLab CI/CD syntax by starting with a basic 3 stage CI/CD pipeline.', - ), - }, + RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME, + RUNNERS_SETTINGS_LINK_CLICKED_EVENT, + RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT, + RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT, + I18N, + inject: ['pipelineEditorPath', 'suggestedCiTemplates'], + props: { + ciRunnerSettingsPath: { + type: String, + required: false, + default: null, }, - templates: { - title: s__('Pipelines|Use a CI/CD template'), - subtitle: s__( - "Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD.", - ), - description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'), + anyRunnersAvailable: { + type: Boolean, + required: false, + default: true, }, }, - inject: ['pipelineEditorPath', 'suggestedCiTemplates'], data() { const templates = this.suggestedCiTemplates.map(({ name, logo }) => { return { name, logo, link: mergeUrlParams({ template: name }, this.pipelineEditorPath), - description: sprintf(this.$options.i18n.templates.description, { name }), + description: sprintf(this.$options.I18N.templates.description, { name }), }; }); @@ -53,39 +62,104 @@ export default { { template: STARTER_TEMPLATE_NAME }, this.pipelineEditorPath, ), + tracker: null, }; }, + computed: { + sharedRunnersHelpPagePath() { + return helpPagePath('ci/runners/runners_scope', { anchor: 'shared-runners' }); + }, + runnersAvailabilitySectionExperimentEnabled() { + return Boolean(getExperimentData(RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME)); + }, + }, + created() { + this.tracker = new ExperimentTracking(RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME); + }, methods: { trackEvent(template) { this.track('template_clicked', { label: template, }); }, + trackExperimentEvent(action) { + this.tracker.event(action); + }, }, }; diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js index c4c2b5f2927..80af3f53245 100644 --- a/app/assets/javascripts/pipelines/pipelines_index.js +++ b/app/assets/javascripts/pipelines/pipelines_index.js @@ -39,6 +39,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { params, codeQualityPagePath, ciRunnerSettingsPath, + anyRunnersAvailable, } = el.dataset; return new Vue({ @@ -78,6 +79,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { params: JSON.parse(params), codeQualityPagePath, ciRunnerSettingsPath, + anyRunnersAvailable: parseBoolean(anyRunnersAvailable), }, }); }, diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 5575cde949b..401fe669373 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -56,6 +56,7 @@ class Projects::PipelinesController < Projects::ApplicationController format.html do enable_code_quality_walkthrough_experiment enable_ci_runner_templates_experiment + enable_runners_availability_section_experiment end format.json do Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) @@ -335,6 +336,18 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def enable_runners_availability_section_experiment + return unless current_user + return unless can?(current_user, :create_pipeline, project) + return if @pipelines_count.to_i > 0 + return if helpers.has_gitlab_ci?(project) + + experiment(:runners_availability_section, namespace: project.root_ancestor) do |e| + e.candidate {} + e.publish_to_database + end + end + def should_track_ci_cd_pipelines? params[:chart].blank? || params[:chart] == 'pipelines' end diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb index 6104a1256d5..0ed43c512ef 100644 --- a/app/helpers/ci/pipelines_helper.rb +++ b/app/helpers/ci/pipelines_helper.rb @@ -78,6 +78,37 @@ module Ci pipeline.stuck? end + def pipelines_list_data(project, list_url) + artifacts_endpoint_placeholder = ':pipeline_artifacts_id' + + data = { + endpoint: list_url, + project_id: project.id, + params: params.to_json, + artifacts_endpoint: downloadable_artifacts_project_pipeline_path(project, artifacts_endpoint_placeholder, format: :json), + artifacts_endpoint_placeholder: artifacts_endpoint_placeholder, + pipeline_schedule_url: pipeline_schedules_path(project), + empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'), + error_state_svg_path: image_path('illustrations/pipelines_failed.svg'), + no_pipelines_svg_path: image_path('illustrations/pipelines_pending.svg'), + can_create_pipeline: can?(current_user, :create_pipeline, project).to_s, + new_pipeline_path: can?(current_user, :create_pipeline, project) && new_project_pipeline_path(project), + ci_lint_path: can?(current_user, :create_pipeline, project) && project_ci_lint_path(project), + reset_cache_path: can?(current_user, :admin_pipeline, project) && reset_cache_project_settings_ci_cd_path(project), + has_gitlab_ci: has_gitlab_ci?(project).to_s, + pipeline_editor_path: can?(current_user, :create_pipeline, project) && project_ci_pipeline_editor_path(project), + suggested_ci_templates: suggested_ci_templates.to_json, + code_quality_page_path: project.present(current_user: current_user).add_code_quality_ci_yml_path, + ci_runner_settings_path: project_settings_ci_cd_path(project, ci_runner_templates: true, anchor: 'js-runners-settings') + } + + experiment(:runners_availability_section, namespace: project.root_ancestor) do |e| + e.candidate { data[:any_runners_available] = project.active_runners.exists?.to_s } + end + + data + end + private def warning_markdown(pipeline) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index ae76d4905e0..2b2c06bd92c 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -1,28 +1,10 @@ - page_title _('Pipelines') - add_page_specific_style 'page_bundles/pipelines' - add_page_specific_style 'page_bundles/ci_status' -- artifacts_endpoint_placeholder = ':pipeline_artifacts_id' = render_if_exists "shared/shared_runners_minutes_limit_flash_message" - list_url = project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough]) - add_page_startup_api_call list_url -#pipelines-list-vue{ data: { endpoint: list_url, - project_id: @project.id, - params: params.to_json, - "artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json), - "artifacts-endpoint-placeholder" => artifacts_endpoint_placeholder, - "pipeline-schedule-url" => pipeline_schedules_path(@project), - "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), - "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), - "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'), - "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, - "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project), - "ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project), - "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project), - "has-gitlab-ci" => has_gitlab_ci?(@project).to_s, - "pipeline-editor-path" => can?(current_user, :create_pipeline, @project) && project_ci_pipeline_editor_path(@project), - "suggested-ci-templates" => suggested_ci_templates.to_json, - "code-quality-page-path" => @project.present(current_user: current_user).add_code_quality_ci_yml_path, - "ci-runner-settings-path" => project_settings_ci_cd_path(@project, ci_runner_templates: true, anchor: 'js-runners-settings') } } +#pipelines-list-vue{ data: pipelines_list_data(@project, list_url) } diff --git a/config/feature_flags/experiment/runners_availability_section.yml b/config/feature_flags/experiment/runners_availability_section.yml new file mode 100644 index 00000000000..41286c94e02 --- /dev/null +++ b/config/feature_flags/experiment/runners_availability_section.yml @@ -0,0 +1,8 @@ +--- +name: runners_availability_section +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80717 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352850 +milestone: '14.9' +type: experiment +group: group::activation +default_enabled: false diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md index cf37abc8d47..61a2121b9c6 100644 --- a/doc/user/application_security/configuration/index.md +++ b/doc/user/application_security/configuration/index.md @@ -68,3 +68,6 @@ You can configure the following security controls: - [License Compliance](../../../user/compliance/license_compliance/index.md) - Can be configured with `.gitlab-ci.yml`. For more details, read [License Compliance](../../../user/compliance/license_compliance/index.md#enable-license-compliance). + +- [Security Training](../../../user/application_security/vulnerabilities/index.md#enable-security-training-for-vulnerabilities) + - Enable **Security training** for the current project. For more details, read [security training](../../../user/application_security/vulnerabilities/index.md#enable-security-training-for-vulnerabilities). diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md index db8b3fe8126..fd96ac303a0 100644 --- a/doc/user/application_security/vulnerabilities/index.md +++ b/doc/user/application_security/vulnerabilities/index.md @@ -30,6 +30,8 @@ On the vulnerability's page, you can: - [Resolve a vulnerability](#resolve-a-vulnerability), if a solution is available. +In GitLab 14.9 and later, if security training is enabled, the vulnerability page includes a training link relevant to the detected vulnerability. + ## Vulnerability status values A vulnerability's status can be one of the following: @@ -159,3 +161,25 @@ To manually apply the patch that GitLab generated for a vulnerability: 1. Ensure your local project has the same commit checked out that was used to generate the patch. 1. Run `git apply remediation.patch`. 1. Verify and commit the changes to your branch. + +## Enable security training for vulnerabilities + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6176) in GitLab 14.9. + +Security training helps your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability. + +To enable security training for vulnerabilities in your project: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Security & Compliance > Configuration**. +1. On the tab bar, select **Vulnerability Management**. +1. To enable a security training provider, turn on the toggle. + +## View security training for a vulnerability + +To view the security training for a vulnerability: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Security & Compliance > Vulnerability report**. +1. Select the vulnerability for which you want to view security training. +1. If the security training provider supports training for the vulnerability, select **View training**. diff --git a/lib/gitlab.rb b/lib/gitlab.rb index ad1a1611309..d03a5add8e8 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -50,7 +50,7 @@ module Gitlab HTTP_PROXY_ENV_VARS = %w(http_proxy https_proxy HTTP_PROXY HTTPS_PROXY).freeze def self.simulate_com? - return false unless Rails.env.test? || Rails.env.development? + return false unless Rails.env.development? Gitlab::Utils.to_boolean(ENV['GITLAB_SIMULATE_SAAS']) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c6cf57d5005..3007b43f754 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -26912,6 +26912,12 @@ msgstr "" msgid "Pipelines settings for '%{project_name}' were successfully updated." msgstr "" +msgid "Pipelines|\"Hello world\" with GitLab CI" +msgstr "" + +msgid "Pipelines|A GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. Install GitLab Runner and register your own runners to get started with CI/CD." +msgstr "" + msgid "Pipelines|API" msgstr "" @@ -26963,7 +26969,7 @@ msgstr "" msgid "Pipelines|Editor" msgstr "" -msgid "Pipelines|Get familiar with GitLab CI/CD syntax by starting with a basic 3 stage CI/CD pipeline." +msgid "Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a \"Hello world\" script to see how it runs, explore how CI/CD works." msgstr "" msgid "Pipelines|Get started with GitLab CI/CD" @@ -26972,12 +26978,18 @@ msgstr "" msgid "Pipelines|GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time consuming tasks, so you can spend more time creating." msgstr "" +msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners." +msgstr "" + msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you." msgstr "" msgid "Pipelines|Improve code quality with GitLab CI/CD" msgstr "" +msgid "Pipelines|Install GitLab Runner" +msgstr "" + msgid "Pipelines|Install GitLab Runners" msgstr "" @@ -26990,6 +27002,9 @@ msgstr "" msgid "Pipelines|Learn about Runners" msgstr "" +msgid "Pipelines|Learn the basics of pipelines and .yml files" +msgstr "" + msgid "Pipelines|Lint" msgstr "" @@ -27005,6 +27020,9 @@ msgstr "" msgid "Pipelines|More Information" msgstr "" +msgid "Pipelines|No runners detected" +msgstr "" + msgid "Pipelines|No triggers have been created yet. Add one using the form above." msgstr "" @@ -27017,9 +27035,15 @@ msgstr "" msgid "Pipelines|Project cache successfully reset." msgstr "" +msgid "Pipelines|Ready to set up CI/CD for your project?" +msgstr "" + msgid "Pipelines|Revoke trigger" msgstr "" +msgid "Pipelines|Runners are available to run your jobs now" +msgstr "" + msgid "Pipelines|Something went wrong while cleaning runners cache." msgstr "" @@ -27083,15 +27107,12 @@ msgstr "" msgid "Pipelines|Trigger user has insufficient permissions to project" msgstr "" -msgid "Pipelines|Use a CI/CD template" +msgid "Pipelines|Try test template" msgstr "" msgid "Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works." msgstr "" -msgid "Pipelines|Use a sample CI/CD template" -msgstr "" - msgid "Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD." msgstr "" diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index d76dfb295a0..689b3dba286 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -116,7 +116,7 @@ module QA end view 'app/views/projects/merge_requests/_mr_box.html.haml' do - element :title_content + element :title_content, required: true end view 'app/views/projects/merge_requests/_mr_title.html.haml' do @@ -124,9 +124,9 @@ module QA end view 'app/views/projects/merge_requests/show.html.haml' do - element :notes_tab - element :commits_tab - element :diffs_tab + element :notes_tab, required: true + element :commits_tab, required: true + element :diffs_tab, required: true end view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue' do diff --git a/qa/qa/page/project/pipeline_editor/new.rb b/qa/qa/page/project/pipeline_editor/new.rb new file mode 100644 index 00000000000..5d79dd86f2a --- /dev/null +++ b/qa/qa/page/project/pipeline_editor/new.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module PipelineEditor + class New < QA::Page::Base + view 'app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue' do + element :create_new_ci_button, required: true + end + + def create_new_ci + click_element(:create_new_ci_button, Page::Project::PipelineEditor::Show) + end + end + end + end + end +end diff --git a/qa/qa/page/project/pipeline_editor/show.rb b/qa/qa/page/project/pipeline_editor/show.rb index 8289039d4c5..e1ce9846292 100644 --- a/qa/qa/page/project/pipeline_editor/show.rb +++ b/qa/qa/page/project/pipeline_editor/show.rb @@ -6,13 +6,13 @@ module QA module PipelineEditor class Show < QA::Page::Base view 'app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue' do - element :branch_selector_button, require: true + element :branch_selector_button, required: true element :branch_menu_item_button element :branch_menu_container end view 'app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue' do - element :target_branch_field, require: true + element :target_branch_field, required: true end view 'app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue' do @@ -21,7 +21,7 @@ module QA end view 'app/assets/javascripts/vue_shared/components/source_editor.vue' do - element :source_editor_container, require: true + element :source_editor_container, required: true end view 'app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue' do @@ -30,6 +30,7 @@ module QA view 'app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue' do element :commit_changes_button + element :new_mr_checkbox end view 'app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue' do @@ -127,6 +128,18 @@ module QA end end + def has_new_mr_checkbox? + has_element?(:new_mr_checkbox, visible: true) + end + + def has_no_new_mr_checkbox? + has_no_element?(:new_mr_checkbox, visible: true) + end + + def select_new_mr_checkbox + check_element(:new_mr_checkbox, true) + end + private def go_to_tab(name) diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb new file mode 100644 index 00000000000..0e7a38626aa --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Verify' do + describe 'Pipeline editor' do + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'pipeline-editor-project' + project.initialize_with_readme = true + end + end + + before do + Flow::Login.sign_in + project.visit! + Page::Project::Menu.perform(&:go_to_pipeline_editor) + end + + after do + project&.remove_via_api! + end + + it( + 'can create merge request', + test_case: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349130' + ) do + Page::Project::PipelineEditor::New.perform(&:create_new_ci) + + Page::Project::PipelineEditor::Show.perform do |show| + # Editor should display default content when project does not have CI file yet + # New MR checkbox should not be rendered when a new target branch is yet to be provided + aggregate_failures 'check editor default conditions' do + expect(show.editing_content).not_to be_empty + expect(show).to have_no_new_mr_checkbox + end + + # The new MR checkbox is visible after a new target branch name is set + show.set_target_branch(SecureRandom.hex(10)) + expect(show).to have_new_mr_checkbox + + show.select_new_mr_checkbox + show.submit_changes + end + + Page::MergeRequest::New.perform(&:create_merge_request) + + Page::MergeRequest::Show.perform do |show| + expect(show).to have_title('Update .gitlab-ci.yml file') + end + end + end + end +end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 4a51e2ed5a0..15783fd990d 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -299,6 +299,10 @@ RSpec.describe Projects::PipelinesController do context 'ci_runner_templates experiment' do it_behaves_like 'tracks assignment and records the subject', :ci_runner_templates, :namespace end + + context 'runners_availability_section experiment' do + it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace + end end describe 'GET #show' do diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 77194fd6ca1..3210f0f9deb 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -1033,71 +1033,6 @@ RSpec.describe 'File blob', :js do stub_feature_flags(refactor_blob_viewer: false) end - context 'when ref switch' do - # We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled - # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351558 - - def switch_ref_to(ref_name) - first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage - - page.within '.project-refs-form' do - click_link ref_name - wait_for_requests - end - end - - context 'when highlighting lines' do - it 'displays single highlighted line number of different ref' do - visit_blob('files/js/application.js', anchor: 'L1') - - switch_ref_to('feature') - - page.within '.blob-content' do - expect(find_by_id('LC1')[:class]).to include("hll") - end - end - - it 'displays multiple highlighted line numbers of different ref' do - visit_blob('files/js/application.js', anchor: 'L1-3') - - switch_ref_to('feature') - - page.within '.blob-content' do - expect(find_by_id('LC1')[:class]).to include("hll") - expect(find_by_id('LC2')[:class]).to include("hll") - expect(find_by_id('LC3')[:class]).to include("hll") - end - end - end - end - - context 'visiting with a line number anchor' do - # We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled - # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351558 - - before do - visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1') - end - - it 'displays the blob using the simple viewer' do - aggregate_failures do - # hides the rich viewer - expect(page).to have_selector('.blob-viewer[data-type="simple"]') - expect(page).not_to have_selector('.blob-viewer[data-type="rich"]') - - # highlights the line in question - expect(page).to have_selector('#LC1.hll') - - # shows highlighted Markdown code - expect(page).to have_css(".js-syntax-highlight") - expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") - - # shows an enabled copy button - expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') - end - end - end - context 'binary file that appears to be text in the first 1024 bytes' do # We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351559 @@ -1158,503 +1093,5 @@ RSpec.describe 'File blob', :js do end end end - - context 'files with auxiliary viewers' do - # This context is the same as the other 'files with auxiliary viewers' in this file, we just ensure that the auxiliary viewers still work this the refactor_blob_viewer disabled - # It should be safe to remove once we rollout the refactored blob viewer - - describe '.gitlab-ci.yml' do - before do - project.add_maintainer(project.creator) - - Files::CreateService.new( - project, - project.creator, - start_branch: 'master', - branch_name: 'master', - commit_message: "Add .gitlab-ci.yml", - file_path: '.gitlab-ci.yml', - file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - ).execute - - visit_blob('.gitlab-ci.yml') - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that configuration is valid - expect(page).to have_content('This GitLab CI configuration is valid.') - - # shows a learn more link - expect(page).to have_link('Learn more') - end - end - end - - describe '.gitlab/route-map.yml' do - before do - project.add_maintainer(project.creator) - - Files::CreateService.new( - project, - project.creator, - start_branch: 'master', - branch_name: 'master', - commit_message: "Add .gitlab/route-map.yml", - file_path: '.gitlab/route-map.yml', - file_content: <<-MAP.strip_heredoc - # Team data - - source: 'data/team.yml' - public: 'team/' - MAP - ).execute - - visit_blob('.gitlab/route-map.yml') - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that map is valid - expect(page).to have_content('This Route Map is valid.') - - # shows a learn more link - expect(page).to have_link('Learn more') - end - end - end - - describe '.gitlab/dashboards/custom-dashboard.yml' do - before do - project.add_maintainer(project.creator) - - Files::CreateService.new( - project, - project.creator, - start_branch: 'master', - branch_name: 'master', - commit_message: "Add .gitlab/dashboards/custom-dashboard.yml", - file_path: '.gitlab/dashboards/custom-dashboard.yml', - file_content: file_content - ).execute - end - - context 'with metrics_dashboard_exhaustive_validations feature flag off' do - before do - stub_feature_flags(metrics_dashboard_exhaustive_validations: false) - visit_blob('.gitlab/dashboards/custom-dashboard.yml') - end - - context 'valid dashboard file' do - let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that dashboard yaml is valid - expect(page).to have_content('Metrics Dashboard YAML definition is valid.') - - # shows a learn more link - expect(page).to have_link('Learn more') - end - end - end - - context 'invalid dashboard file' do - let(:file_content) { "dashboard: 'invalid'" } - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that dashboard yaml is invalid - expect(page).to have_content('Metrics Dashboard YAML definition is invalid:') - expect(page).to have_content("panel_groups: should be an array of panel_groups objects") - - # shows a learn more link - expect(page).to have_link('Learn more') - end - end - end - end - - context 'with metrics_dashboard_exhaustive_validations feature flag on' do - before do - stub_feature_flags(metrics_dashboard_exhaustive_validations: true) - visit_blob('.gitlab/dashboards/custom-dashboard.yml') - end - - context 'valid dashboard file' do - let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that dashboard yaml is valid - expect(page).to have_content('Metrics Dashboard YAML definition is valid.') - - # shows a learn more link - expect(page).to have_link('Learn more') - end - end - end - - context 'invalid dashboard file' do - let(:file_content) { "dashboard: 'invalid'" } - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that dashboard yaml is invalid - expect(page).to have_content('Metrics Dashboard YAML definition is invalid:') - expect(page).to have_content("root is missing required keys: panel_groups") - - # shows a learn more link - expect(page).to have_link('Learn more') - end - end - end - end - end - - context 'LICENSE' do - before do - visit_blob('LICENSE') - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows license - expect(page).to have_content('This project is licensed under the MIT License.') - - # shows a learn more link - expect(page).to have_link('Learn more', href: 'http://choosealicense.com/licenses/mit/') - end - end - end - - context '*.gemspec' do - before do - project.add_maintainer(project.creator) - - Files::CreateService.new( - project, - project.creator, - start_branch: 'master', - branch_name: 'master', - commit_message: "Add activerecord.gemspec", - file_path: 'activerecord.gemspec', - file_content: <<-SPEC.strip_heredoc - Gem::Specification.new do |s| - s.platform = Gem::Platform::RUBY - s.name = "activerecord" - end - SPEC - ).execute - - visit_blob('activerecord.gemspec') - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows names of dependency manager and package - expect(page).to have_content('This project manages its dependencies using RubyGems.') - - # shows a learn more link - expect(page).to have_link('Learn more', href: 'https://rubygems.org/') - end - end - end - - context 'CONTRIBUTING.md' do - before do - file_name = 'CONTRIBUTING.md' - - create_file(file_name, '## Contribution guidelines') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("After you've reviewed these contribution guidelines, you'll be all set to contribute to this project.") - end - end - end - - context 'CHANGELOG.md' do - before do - file_name = 'CHANGELOG.md' - - create_file(file_name, '## Changelog for v1.0.0') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("To find the state of this project's repository at the time of any of these versions, check out the tags.") - end - end - end - - context 'Cargo.toml' do - before do - file_name = 'Cargo.toml' - - create_file(file_name, ' - [package] - name = "hello_world" # the name of the package - version = "0.1.0" # the current version, obeying semver - authors = ["Alice ", "Bob "] - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using Cargo.") - end - end - end - - context 'Cartfile' do - before do - file_name = 'Cartfile' - - create_file(file_name, ' - gitlab "Alamofire/Alamofire" == 4.9.0 - gitlab "Alamofire/AlamofireImage" ~> 3.4 - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using Carthage.") - end - end - end - - context 'composer.json' do - before do - file_name = 'composer.json' - - create_file(file_name, ' - { - "license": "MIT" - } - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using Composer.") - end - end - end - - context 'Gemfile' do - before do - file_name = 'Gemfile' - - create_file(file_name, ' - source "https://rubygems.org" - - # Gems here - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using Bundler.") - end - end - end - - context 'Godeps.json' do - before do - file_name = 'Godeps.json' - - create_file(file_name, ' - { - "GoVersion": "go1.6" - } - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using godep.") - end - end - end - - context 'go.mod' do - before do - file_name = 'go.mod' - - create_file(file_name, ' - module example.com/mymodule - - go 1.14 - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using Go Modules.") - end - end - end - - context 'package.json' do - before do - file_name = 'package.json' - - create_file(file_name, ' - { - "name": "my-awesome-package", - "version": "1.0.0" - } - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using npm.") - end - end - end - - context 'podfile' do - before do - file_name = 'podfile' - - create_file(file_name, 'platform :ios, "8.0"') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using CocoaPods.") - end - end - end - - context 'test.podspec' do - before do - file_name = 'test.podspec' - - create_file(file_name, ' - Pod::Spec.new do |s| - s.name = "TensorFlowLiteC" - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using CocoaPods.") - end - end - end - - context 'JSON.podspec.json' do - before do - file_name = 'JSON.podspec.json' - - create_file(file_name, ' - { - "name": "JSON" - } - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using CocoaPods.") - end - end - end - - context 'requirements.txt' do - before do - file_name = 'requirements.txt' - - create_file(file_name, 'Project requirements') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using pip.") - end - end - end - - context 'yarn.lock' do - before do - file_name = 'yarn.lock' - - create_file(file_name, ' - # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. - # yarn lockfile v1 - ') - visit_blob(file_name) - end - - it 'displays an auxiliary viewer' do - aggregate_failures do - expect(page).to have_content("This project manages its dependencies using Yarn.") - end - end - end - - context 'openapi.yml' do - before do - file_name = 'openapi.yml' - - create_file(file_name, ' - swagger: \'2.0\' - info: - title: Classic API Resource Documentation - description: | -
-

Swagger API documentation

-
- version: production - basePath: /JSSResource/ - produces: - - application/xml - - application/json - consumes: - - application/xml - - application/json - security: - - basicAuth: [] - paths: - /accounts: - get: - responses: - \'200\': - description: No response was specified - tags: - - accounts - operationId: findAccounts - summary: Finds all accounts - ') - visit_blob(file_name, useUnsafeMarkdown: '1') - click_button('Display rendered file') - - wait_for_requests - end - - it 'removes `style`, `class`, and `data-*`` attributes from HTML' do - expect(page).to have_css('h1', text: 'Swagger API documentation') - expect(page).not_to have_css('.foo-bar') - expect(page).not_to have_css('[style="background-color: red;"]') - expect(page).not_to have_css('[data-foo-bar="baz"]') - end - end - end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 83f3b7b3826..2a6cf8634fb 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -913,7 +913,7 @@ RSpec.describe 'Pipelines', :js do end it 'renders empty state' do - expect(page).to have_content 'Use a sample CI/CD template' + expect(page).to have_content 'Try test template' end end end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/balsamiq_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/balsamiq_spec.rb new file mode 100644 index 00000000000..3638e98a08a --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/balsamiq_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Balsamiq file blob', :js do + let(:project) { create(:project, :public, :repository) } + + before do + stub_feature_flags(refactor_blob_viewer: false) + visit project_blob_path(project, 'add-balsamiq-file/files/images/balsamiq.bmpr') + + wait_for_requests + end + + it 'displays Balsamiq file content' do + expect(page).to have_content("Mobile examples") + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_line_permalink_updater_spec.rb new file mode 100644 index 00000000000..e8c026a254e --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_line_permalink_updater_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do + include TreeHelper + + let(:project) { create(:project, :public, :repository) } + let(:path) { 'CHANGELOG' } + let(:sha) { project.repository.commit.sha } + + before do + stub_feature_flags(refactor_blob_viewer: false) + end + + describe 'On a file(blob)' do + def get_absolute_url(path = "") + "http://#{page.server.host}:#{page.server.port}#{path}" + end + + def visit_blob(fragment = nil) + visit project_blob_path(project, tree_join('master', path), anchor: fragment) + end + + describe 'Click "Permalink" button' do + it 'works with no initial line number fragment hash' do + visit_blob + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path)))) + end + + it 'maintains intitial fragment hash' do + fragment = "L3" + + visit_blob(fragment) + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: fragment))) + end + + it 'changes fragment hash if line number clicked' do + ending_fragment = "L5" + + visit_blob + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: ending_fragment))) + end + + it 'with initial fragment hash, changes fragment hash if line number clicked' do + fragment = "L1" + ending_fragment = "L5" + + visit_blob(fragment) + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: ending_fragment))) + end + end + + describe 'Click "Blame" button' do + it 'works with no initial line number fragment hash' do + visit_blob + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path)))) + end + + it 'maintains intitial fragment hash' do + fragment = "L3" + + visit_blob(fragment) + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: fragment))) + end + + it 'changes fragment hash if line number clicked' do + ending_fragment = "L5" + + visit_blob + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: ending_fragment))) + end + + it 'with initial fragment hash, changes fragment hash if line number clicked' do + fragment = "L1" + ending_fragment = "L5" + + visit_blob(fragment) + + find('#L3').click + find("##{ending_fragment}").click + + expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: ending_fragment))) + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_show_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_show_spec.rb new file mode 100644 index 00000000000..659014c922b --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/blob_show_spec.rb @@ -0,0 +1,1154 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'File blob', :js do + include MobileHelpers + + let(:project) { create(:project, :public, :repository) } + + before do + stub_feature_flags(refactor_blob_viewer: false) + end + + def visit_blob(path, anchor: nil, ref: 'master', **additional_args) + visit project_blob_path(project, File.join(ref, path), anchor: anchor, **additional_args) + + wait_for_requests + end + + def create_file(file_name, content) + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add #{file_name}", + file_path: file_name, + file_content: <<-SPEC.strip_heredoc + #{content} + SPEC + ).execute + end + + context 'Ruby file' do + before do + visit_blob('files/ruby/popen.rb') + + wait_for_requests + end + + it 'displays the blob' do + aggregate_failures do + # shows highlighted Ruby code + expect(page).to have_css(".js-syntax-highlight") + expect(page).to have_content("require 'fileutils'") + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + end + end + + it 'displays file actions on all screen sizes' do + file_actions_selector = '.file-actions' + + resize_screen_sm + expect(page).to have_selector(file_actions_selector, visible: true) + + resize_screen_xs + expect(page).to have_selector(file_actions_selector, visible: true) + end + end + + context 'Markdown file' do + context 'visiting directly' do + before do + visit_blob('files/markdown/ruby-style-guide.md') + + wait_for_requests + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows rendered Markdown + expect(page).to have_link("PEP-8") + + # shows a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # shows a disabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_requests + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # shows highlighted Markdown code + expect(page).to have_css(".js-syntax-highlight") + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + + context 'switching to the rich viewer again' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=rich]').click + + wait_for_requests + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end + end + + context 'when ref switch' do + def switch_ref_to(ref_name) + first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage + + page.within '.project-refs-form' do + click_link ref_name + wait_for_requests + end + end + + it 'displays single highlighted line number of different ref' do + visit_blob('files/js/application.js', anchor: 'L1') + + switch_ref_to('feature') + + page.within '.blob-content' do + expect(find_by_id('LC1')[:class]).to include("hll") + end + end + + it 'displays multiple highlighted line numbers of different ref' do + visit_blob('files/js/application.js', anchor: 'L1-3') + + switch_ref_to('feature') + + page.within '.blob-content' do + expect(find_by_id('LC1')[:class]).to include("hll") + expect(find_by_id('LC2')[:class]).to include("hll") + expect(find_by_id('LC3')[:class]).to include("hll") + end + end + + it 'displays no highlighted number of different ref' do + Files::UpdateService.new( + project, + project.first_owner, + commit_message: 'Update', + start_branch: 'feature', + branch_name: 'feature', + file_path: 'files/js/application.js', + file_content: 'new content' + ).execute + + project.commit('feature').diffs.diff_files.first + + visit_blob('files/js/application.js', anchor: 'L3') + switch_ref_to('feature') + + page.within '.blob-content' do + expect(page).not_to have_css('.hll') + end + end + + context 'successfully change ref of similar name' do + before do + project.repository.create_branch('dev') + project.repository.create_branch('development') + end + + it 'switch ref from longer to shorter ref name' do + visit_blob('files/js/application.js', ref: 'development') + switch_ref_to('dev') + + aggregate_failures do + expect(page.find('.file-title-name').text).to eq('application.js') + expect(page).not_to have_css('flash-container') + end + end + + it 'switch ref from shorter to longer ref name' do + visit_blob('files/js/application.js', ref: 'dev') + switch_ref_to('development') + + aggregate_failures do + expect(page.find('.file-title-name').text).to eq('application.js') + expect(page).not_to have_css('flash-container') + end + end + end + + it 'successfully changes ref when the ref name matches the project name' do + project.repository.create_branch(project.name) + + visit_blob('files/js/application.js', ref: project.name) + switch_ref_to('master') + + aggregate_failures do + expect(page.find('.file-title-name').text).to eq('application.js') + expect(page).not_to have_css('flash-container') + end + end + end + + context 'visiting with a line number anchor' do + before do + visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1') + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # highlights the line in question + expect(page).to have_selector('#LC1.hll') + + # shows highlighted Markdown code + expect(page).to have_css(".js-syntax-highlight") + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end + + context 'Markdown rendering' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add RedCarpet and CommonMark Markdown ", + file_path: 'files/commonmark/file.md', + file_content: "1. one\n - sublist\n" + ).execute + end + + context 'when rendering default markdown' do + before do + visit_blob('files/commonmark/file.md') + + wait_for_requests + end + + it 'renders using CommonMark' do + aggregate_failures do + expect(page).to have_content("sublist") + expect(page).not_to have_xpath("//ol//li//ul") + end + end + end + end + + context 'Markdown file (stored in LFS)' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add Markdown in LFS", + file_path: 'files/lfs/file.md', + file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data + ).execute + end + + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_blob('files/lfs/file.md') + + wait_for_requests + end + + it 'displays an error' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows an error message + expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS. You can download it instead.') + + # shows a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_link('Download') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_requests + end + + it 'displays an error' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # shows an error message + expect(page).to have_content('The source could not be displayed because it is stored in LFS. You can download it instead.') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + end + end + end + end + + context 'when LFS is disabled on the project' do + before do + visit_blob('files/lfs/file.md') + + wait_for_requests + end + + it 'displays the blob' do + aggregate_failures do + # shows text + expect(page).to have_content('size 1575078') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + end + end + end + end + + context 'PDF file' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add PDF", + file_path: 'files/test.pdf', + file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data + ).execute + + visit_blob('files/test.pdf') + + wait_for_requests + end + + it 'displays the blob' do + aggregate_failures do + # shows rendered PDF + expect(page).to have_selector('.js-pdf-viewer') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_link('Download') + end + end + end + + context 'Jupiter Notebook file' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add Jupiter Notebook", + file_path: 'files/basic.ipynb', + file_content: project.repository.blob_at('add-ipython-files', 'files/ipython/basic.ipynb').data + ).execute + + visit_blob('files/basic.ipynb') + + wait_for_requests + end + + it 'displays the blob' do + aggregate_failures do + # shows rendered notebook + expect(page).to have_selector('.js-notebook-viewer-mounted') + + # does show a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # show a disabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + + # shows the rendered notebook + expect(page).to have_content('test') + end + end + end + + context 'ISO file (stored in LFS)' do + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_blob('files/lfs/lfs_object.iso') + + wait_for_requests + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (1.5 MB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_link('Download') + end + end + end + + context 'when LFS is disabled on the project' do + before do + visit_blob('files/lfs/lfs_object.iso') + + wait_for_requests + end + + it 'displays the blob' do + aggregate_failures do + # shows text + expect(page).to have_content('size 1575078') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + end + end + end + end + + context 'ZIP file' do + before do + visit_blob('Gemfile.zip') + + wait_for_requests + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (2.11 KB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # shows a download button + expect(page).to have_link('Download') + end + end + end + + context 'empty file' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add empty file", + file_path: 'files/empty.md', + file_content: '' + ).execute + + visit_blob('files/empty.md') + + wait_for_requests + end + + it 'displays an error' do + aggregate_failures do + # shows an error message + expect(page).to have_content('Empty file') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # does not show a copy button + expect(page).not_to have_selector('.js-copy-blob-source-btn') + + # does not show a download or raw button + expect(page).not_to have_link('Download') + expect(page).not_to have_link('Open raw') + end + end + end + + context 'binary file that appears to be text in the first 1024 bytes' do + before do + visit_blob('encoding/binary-1.bin', ref: 'binary-encoding') + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (23.8 KB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # The specs below verify an arguably incorrect result, but since we only + # learn that the file is not actually text once the text viewer content + # is loaded asynchronously, there is no straightforward way to get these + # synchronously loaded elements to display correctly. + # + # Clicking the copy button will result in nothing being copied. + # Clicking the raw button will result in the binary file being downloaded, + # as expected. + + # shows an enabled copy button, incorrectly + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button, incorrectly + expect(page).to have_link('Open raw') + end + end + end + + context 'files with auxiliary viewers' do + describe '.gitlab-ci.yml' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab-ci.yml", + file_path: '.gitlab-ci.yml', + file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + ).execute + + visit_blob('.gitlab-ci.yml') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that configuration is valid + expect(page).to have_content('This GitLab CI configuration is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + describe '.gitlab/route-map.yml' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab/route-map.yml", + file_path: '.gitlab/route-map.yml', + file_content: <<-MAP.strip_heredoc + # Team data + - source: 'data/team.yml' + public: 'team/' + MAP + ).execute + + visit_blob('.gitlab/route-map.yml') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that map is valid + expect(page).to have_content('This Route Map is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + describe '.gitlab/dashboards/custom-dashboard.yml' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab/dashboards/custom-dashboard.yml", + file_path: '.gitlab/dashboards/custom-dashboard.yml', + file_content: file_content + ).execute + end + + context 'with metrics_dashboard_exhaustive_validations feature flag off' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) + visit_blob('.gitlab/dashboards/custom-dashboard.yml') + end + + context 'valid dashboard file' do + let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is valid + expect(page).to have_content('Metrics Dashboard YAML definition is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + context 'invalid dashboard file' do + let(:file_content) { "dashboard: 'invalid'" } + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is invalid + expect(page).to have_content('Metrics Dashboard YAML definition is invalid:') + expect(page).to have_content("panel_groups: should be an array of panel_groups objects") + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + end + + context 'with metrics_dashboard_exhaustive_validations feature flag on' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) + visit_blob('.gitlab/dashboards/custom-dashboard.yml') + end + + context 'valid dashboard file' do + let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is valid + expect(page).to have_content('Metrics Dashboard YAML definition is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + context 'invalid dashboard file' do + let(:file_content) { "dashboard: 'invalid'" } + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is invalid + expect(page).to have_content('Metrics Dashboard YAML definition is invalid:') + expect(page).to have_content("root is missing required keys: panel_groups") + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + end + end + + context 'LICENSE' do + before do + visit_blob('LICENSE') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows license + expect(page).to have_content('This project is licensed under the MIT License.') + + # shows a learn more link + expect(page).to have_link('Learn more', href: 'http://choosealicense.com/licenses/mit/') + end + end + end + + context '*.gemspec' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add activerecord.gemspec", + file_path: 'activerecord.gemspec', + file_content: <<-SPEC.strip_heredoc + Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = "activerecord" + end + SPEC + ).execute + + visit_blob('activerecord.gemspec') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows names of dependency manager and package + expect(page).to have_content('This project manages its dependencies using RubyGems.') + + # shows a learn more link + expect(page).to have_link('Learn more', href: 'https://rubygems.org/') + end + end + end + + context 'CONTRIBUTING.md' do + before do + file_name = 'CONTRIBUTING.md' + + create_file(file_name, '## Contribution guidelines') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("After you've reviewed these contribution guidelines, you'll be all set to contribute to this project.") + end + end + end + + context 'CHANGELOG.md' do + before do + file_name = 'CHANGELOG.md' + + create_file(file_name, '## Changelog for v1.0.0') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("To find the state of this project's repository at the time of any of these versions, check out the tags.") + end + end + end + + context 'Cargo.toml' do + before do + file_name = 'Cargo.toml' + + create_file(file_name, ' + [package] + name = "hello_world" # the name of the package + version = "0.1.0" # the current version, obeying semver + authors = ["Alice ", "Bob "] + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Cargo.") + end + end + end + + context 'Cartfile' do + before do + file_name = 'Cartfile' + + create_file(file_name, ' + gitlab "Alamofire/Alamofire" == 4.9.0 + gitlab "Alamofire/AlamofireImage" ~> 3.4 + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Carthage.") + end + end + end + + context 'composer.json' do + before do + file_name = 'composer.json' + + create_file(file_name, ' + { + "license": "MIT" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Composer.") + end + end + end + + context 'Gemfile' do + before do + file_name = 'Gemfile' + + create_file(file_name, ' + source "https://rubygems.org" + + # Gems here + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Bundler.") + end + end + end + + context 'Godeps.json' do + before do + file_name = 'Godeps.json' + + create_file(file_name, ' + { + "GoVersion": "go1.6" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using godep.") + end + end + end + + context 'go.mod' do + before do + file_name = 'go.mod' + + create_file(file_name, ' + module example.com/mymodule + + go 1.14 + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Go Modules.") + end + end + end + + context 'package.json' do + before do + file_name = 'package.json' + + create_file(file_name, ' + { + "name": "my-awesome-package", + "version": "1.0.0" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using npm.") + end + end + end + + context 'podfile' do + before do + file_name = 'podfile' + + create_file(file_name, 'platform :ios, "8.0"') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using CocoaPods.") + end + end + end + + context 'test.podspec' do + before do + file_name = 'test.podspec' + + create_file(file_name, ' + Pod::Spec.new do |s| + s.name = "TensorFlowLiteC" + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using CocoaPods.") + end + end + end + + context 'JSON.podspec.json' do + before do + file_name = 'JSON.podspec.json' + + create_file(file_name, ' + { + "name": "JSON" + } + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using CocoaPods.") + end + end + end + + context 'requirements.txt' do + before do + file_name = 'requirements.txt' + + create_file(file_name, 'Project requirements') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using pip.") + end + end + end + + context 'yarn.lock' do + before do + file_name = 'yarn.lock' + + create_file(file_name, ' + # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + # yarn lockfile v1 + ') + visit_blob(file_name) + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + expect(page).to have_content("This project manages its dependencies using Yarn.") + end + end + end + end + + context 'realtime pipelines' do + before do + Files::CreateService.new( + project, + project.creator, + start_branch: 'feature', + branch_name: 'feature', + commit_message: "Add ruby file", + file_path: 'files/ruby/test.rb', + file_content: "# Awesome content" + ).execute + + create(:ci_pipeline, status: 'running', project: project, ref: 'feature', sha: project.commit('feature').sha) + visit_blob('files/ruby/test.rb', ref: 'feature') + end + + it 'shows the realtime pipeline status' do + page.within('.commit-actions') do + expect(page).to have_css('.ci-status-icon') + expect(page).to have_css('.ci-status-icon-running') + expect(page).to have_css('.js-ci-status-icon-running') + end + end + end + + context 'for subgroups' do + let(:group) { create(:group) } + let(:subgroup) { create(:group, parent: group) } + let(:project) { create(:project, :public, :repository, group: subgroup) } + + it 'renders tree table without errors' do + visit_blob('README.md') + + expect(page).to have_selector('.file-content') + expect(page).not_to have_selector('[data-testid="alert-danger"]') + end + + it 'displays a GPG badge' do + visit_blob('CONTRIBUTING.md', ref: '33f3729a45c02fc67d00adb1b8bca394b0e761d9') + + expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge' + expect(page).to have_selector '.gpg-status-box.invalid' + end + end + + context 'on signed merge commit' do + it 'displays a GPG badge' do + visit_blob('conflicting-file.md', ref: '6101e87e575de14b38b4e1ce180519a813671e10') + + expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge' + expect(page).to have_selector '.gpg-status-box.invalid' + end + end + + context 'when static objects external storage is enabled' do + before do + stub_application_setting(static_objects_external_storage_url: 'https://cdn.gitlab.com') + end + + context 'private project' do + let_it_be(:project) { create(:project, :repository, :private) } + let_it_be(:user) { create(:user) } + + before do + project.add_developer(user) + + sign_in(user) + visit_blob('README.md') + end + + it 'shows open raw and download buttons with external storage URL prepended and user token appended to their href' do + path = project_raw_path(project, 'master/README.md') + raw_uri = "https://cdn.gitlab.com#{path}?token=#{user.static_object_token}" + download_uri = "https://cdn.gitlab.com#{path}?inline=false&token=#{user.static_object_token}" + + aggregate_failures do + expect(page).to have_link 'Open raw', href: raw_uri + expect(page).to have_link 'Download', href: download_uri + end + end + end + + context 'public project' do + before do + visit_blob('README.md') + end + + it 'shows open raw and download buttons with external storage URL prepended to their href' do + path = project_raw_path(project, 'master/README.md') + raw_uri = "https://cdn.gitlab.com#{path}" + download_uri = "https://cdn.gitlab.com#{path}?inline=false" + + aggregate_failures do + expect(page).to have_link 'Open raw', href: raw_uri + expect(page).to have_link 'Download', href: download_uri + end + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/edit_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/edit_spec.rb new file mode 100644 index 00000000000..f5b9947b29e --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/edit_spec.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Editing file blob', :js do + include TreeHelper + include BlobSpecHelpers + + let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') } + let(:branch) { 'master' } + let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] } + let(:readme_file_path) { 'README.md' } + + before do + stub_feature_flags(refactor_blob_viewer: false) + end + + context 'as a developer' do + let(:user) { create(:user) } + let(:role) { :developer } + + before do + project.add_role(user, role) + sign_in(user) + end + + def edit_and_commit(commit_changes: true, is_diff: false) + set_default_button('edit') + refresh + wait_for_requests + + if is_diff + first('.js-diff-more-actions').click + click_link('Edit in single-file editor') + else + click_link('Edit') + end + + fill_editor(content: 'class NextFeature\\nend\\n') + + if commit_changes + click_button 'Commit changes' + end + end + + def fill_editor(content: 'class NextFeature\\nend\\n') + wait_for_requests + execute_script("monaco.editor.getModels()[0].setValue('#{content}')") + end + + context 'from MR diff' do + before do + visit diffs_project_merge_request_path(project, merge_request) + edit_and_commit(is_diff: true) + end + + it 'returns me to the mr' do + expect(page).to have_content(merge_request.title) + end + end + + it 'updates the content of file with a number as file path' do + project.repository.create_file(user, '1', 'test', message: 'testing', branch_name: branch) + visit project_blob_path(project, tree_join(branch, '1')) + + edit_and_commit + + expect(page).to have_content 'NextFeature' + end + + it 'editing a template file in a sub directory does not change path' do + project.repository.create_file(user, 'ci/.gitlab-ci.yml', 'test', message: 'testing', branch_name: branch) + visit project_edit_blob_path(project, tree_join(branch, 'ci/.gitlab-ci.yml')) + + expect(find_by_id('file_path').value).to eq('ci/.gitlab-ci.yml') + end + + it 'updating file path updates syntax highlighting' do + visit project_edit_blob_path(project, tree_join(branch, readme_file_path)) + expect(find('#editor')['data-mode-id']).to eq('markdown') + + find('#file_path').send_keys('foo.txt') do + expect(find('#editor')['data-mode-id']).to eq('plaintext') + end + end + + context 'from blob file path' do + before do + visit project_blob_path(project, tree_join(branch, file_path)) + end + + it 'updates content' do + edit_and_commit + + expect(page).to have_content 'successfully committed' + expect(page).to have_content 'NextFeature' + end + + it 'previews content' do + edit_and_commit(commit_changes: false) + click_link 'Preview changes' + wait_for_requests + + old_line_count = page.all('.line_holder.old').size + new_line_count = page.all('.line_holder.new').size + + expect(old_line_count).to be > 0 + expect(new_line_count).to be > 0 + end + end + + context 'when rendering the preview' do + it 'renders content with CommonMark' do + visit project_edit_blob_path(project, tree_join(branch, readme_file_path)) + fill_editor(content: '1. one\\n - sublist\\n') + click_link 'Preview' + wait_for_requests + + # the above generates two separate lists (not embedded) in CommonMark + expect(page).to have_content('sublist') + expect(page).not_to have_xpath('//ol//li//ul') + end + end + end + + context 'visit blob edit' do + context 'redirects to sign in and returns' do + context 'as developer' do + let(:user) { create(:user) } + + before do + project.add_developer(user) + visit project_edit_blob_path(project, tree_join(branch, file_path)) + end + + it 'redirects to sign in and returns' do + expect(page).to have_current_path(new_user_session_path) + + gitlab_sign_in(user) + + expect(page).to have_current_path(project_edit_blob_path(project, tree_join(branch, file_path))) + end + end + + context 'as guest' do + let(:user) { create(:user) } + + before do + visit project_edit_blob_path(project, tree_join(branch, file_path)) + end + + it 'redirects to sign in and returns' do + expect(page).to have_current_path(new_user_session_path) + + gitlab_sign_in(user) + + expect(page).to have_current_path(project_blob_path(project, tree_join(branch, file_path))) + end + end + end + + context 'as developer' do + let(:user) { create(:user) } + let(:protected_branch) { 'protected-branch' } + + before do + project.add_developer(user) + project.repository.add_branch(user, protected_branch, 'master') + create(:protected_branch, project: project, name: protected_branch) + sign_in(user) + end + + context 'on some branch' do + before do + visit project_edit_blob_path(project, tree_join(branch, file_path)) + end + + it 'shows blob editor with same branch' do + expect(page).to have_current_path(project_edit_blob_path(project, tree_join(branch, file_path))) + expect(find('.js-branch-name').value).to eq(branch) + end + end + + context 'with protected branch' do + it 'shows blob editor with patch branch' do + freeze_time do + visit project_edit_blob_path(project, tree_join(protected_branch, file_path)) + + epoch = Time.zone.now.strftime('%s%L').last(5) + + expect(find('.js-branch-name').value).to eq "#{user.username}-protected-branch-patch-#{epoch}" + end + end + end + end + + context 'as maintainer' do + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + visit project_edit_blob_path(project, tree_join(branch, file_path)) + end + + it 'shows blob editor with same branch' do + expect(page).to have_current_path(project_edit_blob_path(project, tree_join(branch, file_path))) + expect(find('.js-branch-name').value).to eq(branch) + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/shortcuts_blob_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/shortcuts_blob_spec.rb new file mode 100644 index 00000000000..fe0b217992e --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/shortcuts_blob_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Blob shortcuts', :js do + include TreeHelper + let(:project) { create(:project, :public, :repository) } + let(:path) { project.repository.ls_files(project.repository.root_ref)[0] } + let(:sha) { project.repository.commit.sha } + + before do + stub_feature_flags(refactor_blob_viewer: false) + end + + describe 'On a file(blob)', :js do + def get_absolute_url(path = "") + "http://#{page.server.host}:#{page.server.port}#{path}" + end + + def visit_blob(fragment = nil) + visit project_blob_path(project, tree_join('master', path), anchor: fragment) + end + + describe 'pressing "y"' do + it 'redirects to permalink with commit sha' do + visit_blob + wait_for_requests + + find('body').native.send_key('y') + + expect(page).to have_current_path(get_absolute_url(project_blob_path(project, tree_join(sha, path))), url: true) + end + + it 'maintains fragment hash when redirecting' do + fragment = "L1" + visit_blob(fragment) + wait_for_requests + + find('body').native.send_key('y') + + expect(page).to have_current_path(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: fragment)), url: true) + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_creates_new_blob_in_new_project_spec.rb new file mode 100644 index 00000000000..fe38659f60b --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_creates_new_blob_in_new_project_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User creates new blob', :js do + include WebIdeSpecHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :empty_repo) } + + before do + stub_feature_flags(refactor_blob_viewer: false) + end + + shared_examples 'creating a file' do + it 'allows the user to add a new file in Web IDE' do + visit project_path(project) + + click_link 'New file' + + wait_for_requests + + ide_create_new_file('dummy-file', content: "Hello world\n") + + ide_commit + + expect(page).to have_content('All changes are committed') + expect(project.repository.blob_at('master', 'dummy-file').data).to eql("Hello world\n") + end + end + + describe 'as a maintainer' do + before do + project.add_maintainer(user) + sign_in(user) + end + + it_behaves_like 'creating a file' + end + + describe 'as an admin' do + let(:user) { create(:user, :admin) } + + before do + sign_in(user) + gitlab_enable_admin_mode_sign_in(user) + end + + it_behaves_like 'creating a file' + end + + describe 'as a developer' do + before do + project.add_developer(user) + sign_in(user) + visit project_path(project) + end + + it 'does not allow pushing to the default branch' do + expect(page).not_to have_content('New file') + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb new file mode 100644 index 00000000000..4290df08e66 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled', :js do + include CookieHelper + + let(:project) { create(:project, :empty_repo) } + let(:user) { project.first_owner } + + before do + stub_feature_flags(refactor_blob_viewer: false) + end + + describe 'viewing the new blob page' do + before do + sign_in(user) + end + + context 'when the page is loaded from the link using the suggest_gitlab_ci_yml param' do + before do + visit namespace_project_new_blob_path(namespace_id: project.namespace, project_id: project, id: 'master', suggest_gitlab_ci_yml: 'true') + end + + it 'pre-fills .gitlab-ci.yml for file name' do + file_name = page.find_by_id('file_name') + + expect(file_name.value).to have_content('.gitlab-ci.yml') + end + + it 'chooses the .gitlab-ci.yml Template Type' do + template_type = page.find(:css, '.template-type-selector .dropdown-toggle-text') + + expect(template_type.text).to have_content('.gitlab-ci.yml') + end + + it 'displays suggest_gitlab_ci_yml popover' do + page.find(:css, '.gitlab-ci-yml-selector').click + + popover_selector = '.suggest-gitlab-ci-yml' + + expect(page).to have_css(popover_selector, visible: true) + + page.within(popover_selector) do + expect(page).to have_content('1/2: Choose a template') + end + end + + it 'sets the commit cookie when the Commit button is clicked' do + click_button 'Commit changes' + + expect(get_cookie("suggest_gitlab_ci_yml_commit_#{project.id}")).to be_present + end + end + + context 'when the page is visited without the param' do + before do + visit namespace_project_new_blob_path(namespace_id: project.namespace, project_id: project, id: 'master') + end + + it 'does not pre-fill .gitlab-ci.yml for file name' do + file_name = page.find_by_id('file_name') + + expect(file_name.value).not_to have_content('.gitlab-ci.yml') + end + + it 'does not choose the .gitlab-ci.yml Template Type' do + template_type = page.find(:css, '.template-type-selector .dropdown-toggle-text') + + expect(template_type.text).to have_content('Select a template type') + end + + it 'does not display suggest_gitlab_ci_yml popover' do + popover_selector = '.b-popover.suggest-gitlab-ci-yml' + + expect(page).not_to have_css(popover_selector, visible: true) + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_views_pipeline_editor_button_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_views_pipeline_editor_button_spec.rb new file mode 100644 index 00000000000..a00e1eaa551 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/blobs/user_views_pipeline_editor_button_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User views pipeline editor button on root ci config file', :js do + include BlobSpecHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + + before do + stub_feature_flags(refactor_blob_viewer: false) + end + + context "when the ci config is the root file" do + before do + project.add_developer(user) + sign_in(user) + end + + it 'shows the button to the Pipeline Editor' do + project.update!(ci_config_path: '.my-config.yml') + project.repository.create_file(user, project.ci_config_path_or_default, 'test', message: 'testing', branch_name: 'master') + visit project_blob_path(project, File.join('master', '.my-config.yml')) + + expect(page).to have_content('Edit in pipeline editor') + end + + it 'does not shows the Pipeline Editor button' do + project.repository.create_file(user, '.my-sub-config.yml', 'test', message: 'testing', branch_name: 'master') + visit project_blob_path(project, File.join('master', '.my-sub-config.yml')) + + expect(page).not_to have_content('Edit in pipeline editor') + end + end + + context "when user cannot collaborate" do + before do + sign_in(user) + end + it 'does not shows the Pipeline Editor button' do + visit project_blob_path(project, File.join('master', '.my-config.yml')) + expect(page).not_to have_content('Edit in pipeline editor') + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/editing_a_file_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/editing_a_file_spec.rb new file mode 100644 index 00000000000..c32fb1aa4d3 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/editing_a_file_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Files > User wants to edit a file' do + let(:project) { create(:project, :repository) } + let(:user) { project.first_owner } + let(:commit_params) do + { + start_branch: project.default_branch, + branch_name: project.default_branch, + commit_message: "Committing First Update", + file_path: ".gitignore", + file_content: "First Update", + last_commit_sha: Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, + ".gitignore").sha + } + end + + before do + stub_feature_flags(refactor_blob_viewer: false) + sign_in user + visit project_edit_blob_path(project, + File.join(project.default_branch, '.gitignore')) + end + + it 'file has been updated since the user opened the edit page' do + Files::UpdateService.new(project, user, commit_params).execute + + click_button 'Commit changes' + + expect(page).to have_content 'Someone edited the file the same time you did.' + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/find_file_keyboard_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/find_file_keyboard_spec.rb new file mode 100644 index 00000000000..9ba5f5a9b57 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/find_file_keyboard_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Files > Find file keyboard shortcuts', :js do + let(:project) { create(:project, :repository) } + let(:user) { project.first_owner } + + before do + stub_feature_flags(refactor_blob_viewer: false) + sign_in user + + visit project_find_file_path(project, project.repository.root_ref) + + wait_for_requests + end + + it 'opens file when pressing enter key' do + fill_in 'file_find', with: 'CHANGELOG' + + find('#file_find').native.send_keys(:enter) + + expect(page).to have_selector('.blob-content-holder') + + page.within('.js-file-title') do + expect(page).to have_content('CHANGELOG') + end + end + + it 'navigates files with arrow keys' do + fill_in 'file_find', with: 'application.' + + find('#file_find').native.send_keys(:down) + find('#file_find').native.send_keys(:enter) + + expect(page).to have_selector('.blob-content-holder') + + page.within('.js-file-title') do + expect(page).to have_content('application.js') + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/project_owner_creates_license_file_spec.rb new file mode 100644 index 00000000000..ab920504100 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/project_owner_creates_license_file_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Files > Project owner creates a license file', :js do + let(:project) { create(:project, :repository) } + let(:project_maintainer) { project.first_owner } + + before do + stub_feature_flags(refactor_blob_viewer: false) + project.repository.delete_file(project_maintainer, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') + sign_in(project_maintainer) + visit project_path(project) + end + + it 'project maintainer creates a license file manually from a template' do + visit project_tree_path(project, project.repository.root_ref) + find('.add-to-tree').click + click_link 'New file' + + fill_in :file_name, with: 'LICENSE' + + expect(page).to have_selector('.license-selector') + + select_template('MIT License') + + file_content = first('.file-editor') + expect(file_content).to have_content('MIT License') + expect(file_content).to have_content("Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}") + + fill_in :commit_message, with: 'Add a LICENSE file', visible: true + click_button 'Commit changes' + + expect(page).to have_current_path( + project_blob_path(project, 'master/LICENSE'), ignore_query: true) + expect(page).to have_content('MIT License') + expect(page).to have_content("Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}") + end + + it 'project maintainer creates a license file from the "Add license" link' do + click_link 'Add LICENSE' + + expect(page).to have_content('New file') + expect(page).to have_current_path( + project_new_blob_path(project, 'master'), ignore_query: true) + expect(find('#file_name').value).to eq('LICENSE') + expect(page).to have_selector('.license-selector') + + select_template('MIT License') + + file_content = first('.file-editor') + expect(file_content).to have_content('MIT License') + expect(file_content).to have_content("Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}") + + fill_in :commit_message, with: 'Add a LICENSE file', visible: true + click_button 'Commit changes' + + expect(page).to have_current_path( + project_blob_path(project, 'master/LICENSE'), ignore_query: true) + expect(page).to have_content('MIT License') + expect(page).to have_content("Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}") + end + + def select_template(template) + page.within('.js-license-selector-wrap') do + click_button 'Apply a template' + click_link template + wait_for_requests + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_files_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_files_spec.rb new file mode 100644 index 00000000000..5abdad905fd --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_files_spec.rb @@ -0,0 +1,377 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "User browses files", :js do + include RepoHelpers + + let(:fork_message) do + "You're not allowed to make changes to this project directly. "\ + "A fork of this project has been created that you can make changes in, so you can submit a merge request." + end + + let(:project) { create(:project, :repository, name: "Shop") } + let(:project2) { create(:project, :repository, name: "Another Project", path: "another-project") } + let(:tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } + let(:user) { project.first_owner } + + before do + stub_feature_flags(refactor_blob_viewer: false) + sign_in(user) + end + + it "shows last commit for current directory", :js do + visit(tree_path_root_ref) + + click_link("files") + + last_commit = project.repository.last_commit_for_path(project.default_branch, "files") + + page.within(".commit-detail") do + expect(page).to have_content(last_commit.short_id).and have_content(last_commit.author_name) + end + end + + context "when browsing the master branch", :js do + before do + visit(tree_path_root_ref) + end + + it "shows files from a repository" do + expect(page).to have_content("VERSION") + .and have_content(".gitignore") + .and have_content("LICENSE") + end + + it "shows the `Browse Directory` link" do + click_link("files") + + page.within('.repo-breadcrumb') do + expect(page).to have_link('files') + end + + click_link("History") + + expect(page).to have_link("Browse Directory").and have_no_link("Browse Code") + end + + it "shows the `Browse File` link" do + page.within(".tree-table") do + click_link("README.md") + end + + click_link("History") + + expect(page).to have_link("Browse File").and have_no_link("Browse Files") + end + + it "shows the `Browse Files` link" do + click_link("History") + + expect(page).to have_link("Browse Files").and have_no_link("Browse Directory") + end + + it "redirects to the permalink URL" do + click_link(".gitignore") + click_link("Permalink") + + permalink_path = project_blob_path(project, "#{project.repository.commit.sha}/.gitignore") + + expect(page).to have_current_path(permalink_path, ignore_query: true) + end + end + + context "when browsing the `markdown` branch", :js do + context "when browsing the root" do + before do + visit(project_tree_path(project, "markdown")) + end + + it "shows correct files and links" do + expect(page).to have_current_path(project_tree_path(project, "markdown"), ignore_query: true) + expect(page).to have_content("README.md") + .and have_content("CHANGELOG") + .and have_content("Welcome to GitLab GitLab is a free project and repository management application") + .and have_link("GitLab API doc") + .and have_link("GitLab API website") + .and have_link("Rake tasks") + .and have_link("backup and restore procedure") + .and have_link("GitLab API doc directory") + .and have_link("Maintenance") + .and have_header_with_correct_id_and_link(2, "Application details", "application-details") + .and have_link("empty", href: "") + .and have_link("#id", href: "#id") + .and have_link("/#id", href: project_blob_path(project, "markdown/README.md", anchor: "id")) + .and have_link("README.md#id", href: project_blob_path(project, "markdown/README.md", anchor: "id")) + .and have_link("d/README.md#id", href: project_blob_path(project, "markdown/db/README.md", anchor: "id")) + end + + it "shows correct content of file" do + click_link("GitLab API doc") + + expect(page).to have_current_path(project_blob_path(project, "markdown/doc/api/README.md"), ignore_query: true) + expect(page).to have_content("All API requests require authentication") + .and have_content("Contents") + .and have_link("Users") + .and have_link("Rake tasks") + .and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api") + + click_link("Users") + + expect(page).to have_current_path(project_blob_path(project, "markdown/doc/api/users.md"), ignore_query: true) + expect(page).to have_content("Get a list of users.") + + page.go_back + + click_link("Rake tasks") + + expect(page).to have_current_path(project_tree_path(project, "markdown/doc/raketasks"), ignore_query: true) + expect(page).to have_content("backup_restore.md").and have_content("maintenance.md") + + click_link("maintenance.md") + + expect(page).to have_current_path(project_blob_path(project, "markdown/doc/raketasks/maintenance.md"), ignore_query: true) + expect(page).to have_content("bundle exec rake gitlab:env:info RAILS_ENV=production") + + click_link("shop") + + page.within(".tree-table") do + click_link("README.md") + end + + page.go_back + + page.within(".tree-table") do + click_link("d") + end + + expect(page).to have_link("..", href: project_tree_path(project, "markdown/")) + + page.within(".tree-table") do + click_link("README.md") + end + + expect(page).to have_link("empty", href: "") + end + + it "shows correct content of directory" do + click_link("GitLab API doc directory") + + expect(page).to have_current_path(project_tree_path(project, "markdown/doc/api"), ignore_query: true) + expect(page).to have_content("README.md").and have_content("users.md") + + click_link("Users") + + expect(page).to have_current_path(project_blob_path(project, "markdown/doc/api/users.md"), ignore_query: true) + expect(page).to have_content("List users").and have_content("Get a list of users.") + end + end + end + + context 'when commit message has markdown', :js do + before do + project.repository.create_file(user, 'index', 'test', message: ':star: testing', branch_name: 'master') + + visit(project_tree_path(project, "master")) + end + + it 'renders emojis' do + expect(page).to have_selector('gl-emoji', count: 2) + end + end + + context "when browsing a `improve/awesome` branch", :js do + before do + visit(project_tree_path(project, "improve/awesome")) + end + + it "shows files from a repository" do + expect(page).to have_content("VERSION") + .and have_content(".gitignore") + .and have_content("LICENSE") + + click_link("files") + + page.within('.repo-breadcrumb') do + expect(page).to have_link('files') + end + + click_link("html") + + page.within('.repo-breadcrumb') do + expect(page).to have_link('html') + end + + expect(page).to have_link('500.html') + end + end + + context "when browsing a `Ääh-test-utf-8` branch", :js do + before do + project.repository.create_branch('Ääh-test-utf-8', project.repository.root_ref) + visit(project_tree_path(project, "Ääh-test-utf-8")) + end + + it "shows files from a repository" do + expect(page).to have_content("VERSION") + .and have_content(".gitignore") + .and have_content("LICENSE") + + click_link("files") + + page.within('.repo-breadcrumb') do + expect(page).to have_link('files') + end + + click_link("html") + + page.within('.repo-breadcrumb') do + expect(page).to have_link('html') + end + + expect(page).to have_link('500.html') + end + end + + context "when browsing a `test-#` branch", :js do + before do + project.repository.create_branch('test-#', project.repository.root_ref) + visit(project_tree_path(project, "test-#")) + end + + it "shows files from a repository" do + expect(page).to have_content("VERSION") + .and have_content(".gitignore") + .and have_content("LICENSE") + + click_link("files") + + page.within('.repo-breadcrumb') do + expect(page).to have_link('files') + end + + click_link("html") + + page.within('.repo-breadcrumb') do + expect(page).to have_link('html') + end + + expect(page).to have_link('500.html') + end + end + + context "when browsing a specific ref", :js do + let(:ref) { project_tree_path(project, "6d39438") } + + before do + visit(ref) + end + + it "shows files from a repository for `6d39438`" do + expect(page).to have_current_path(ref, ignore_query: true) + expect(page).to have_content(".gitignore").and have_content("LICENSE") + end + + it "shows files from a repository with apostroph in its name" do + first(".js-project-refs-dropdown").click + + page.within(".project-refs-form") do + click_link("'test'") + end + + expect(page).to have_selector(".dropdown-toggle-text", text: "'test'") + + visit(project_tree_path(project, "'test'")) + + expect(page).not_to have_selector(".tree-commit .animation-container") + end + + it "shows the code with a leading dot in the directory" do + first(".js-project-refs-dropdown").click + + page.within(".project-refs-form") do + click_link("fix") + end + + visit(project_tree_path(project, "fix/.testdir")) + + expect(page).not_to have_selector(".tree-commit .animation-container") + end + + it "does not show the permalink link" do + click_link(".gitignore") + + expect(page).not_to have_link("permalink") + end + end + + context "when browsing a file content", :js do + before do + visit(tree_path_root_ref) + wait_for_requests + + click_link(".gitignore") + end + + it "shows a file content" do + expect(page).to have_content("*.rbc") + end + + it "is possible to blame" do + click_link("Blame") + + expect(page).to have_content("*.rb") + .and have_content("Dmitriy Zaporozhets") + .and have_content("Initial commit") + .and have_content("Ignore DS files") + + previous_commit_anchor = "//a[@title='Ignore DS files']/parent::span/following-sibling::span/a" + find(:xpath, previous_commit_anchor).click + + expect(page).to have_content("*.rb") + .and have_content("Dmitriy Zaporozhets") + .and have_content("Initial commit") + + expect(page).not_to have_content("Ignore DS files") + end + end + + context "when browsing a file with pathspec characters" do + let(:filename) { ':wq' } + let(:newrev) { project.repository.commit('master').sha } + + before do + create_file_in_repo(project, 'master', 'master', filename, 'Test file') + path = File.join('master', filename) + + visit(project_blob_path(project, path)) + wait_for_requests + end + + it "shows raw file content in a new tab" do + new_tab = window_opened_by {click_link 'Open raw'} + + within_window new_tab do + expect(page).to have_content("Test file") + end + end + end + + context "when browsing a raw file" do + before do + visit(tree_path_root_ref) + wait_for_requests + + click_link(".gitignore") + wait_for_requests + end + + it "shows raw file content in a new tab" do + new_tab = window_opened_by {click_link 'Open raw'} + + within_window new_tab do + expect(page).to have_content("*.rbc") + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_lfs_files_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_lfs_files_spec.rb new file mode 100644 index 00000000000..2d9b6b3a903 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/user_browses_lfs_files_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Files > User browses LFS files' do + let(:project) { create(:project, :repository) } + let(:user) { project.first_owner } + + before do + stub_feature_flags(refactor_blob_viewer: false) + sign_in(user) + end + + context 'when LFS is disabled', :js do + before do + allow_next_found_instance_of(Project) do |project| + allow(project).to receive(:lfs_enabled?).and_return(false) + end + + visit project_tree_path(project, 'lfs') + wait_for_requests + end + + it 'is possible to see raw content of LFS pointer' do + click_link 'files' + + page.within('.repo-breadcrumb') do + expect(page).to have_link('files') + end + + click_link 'lfs' + + page.within('.repo-breadcrumb') do + expect(page).to have_link('lfs') + end + + click_link 'lfs_object.iso' + + expect(page).to have_content 'version https://git-lfs.github.com/spec/v1' + expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' + expect(page).to have_content 'size 1575078' + expect(page).not_to have_content 'Download (1.5 MB)' + end + end + + context 'when LFS is enabled', :js do + before do + allow_next_found_instance_of(Project) do |project| + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + visit project_tree_path(project, 'lfs') + wait_for_requests + end + + it 'shows an LFS object' do + click_link('files') + + page.within('.repo-breadcrumb') do + expect(page).to have_link('files') + end + + click_link('lfs') + click_link('lfs_object.iso') + + expect(page).to have_content('Download (1.5 MB)') + expect(page).not_to have_content('version https://git-lfs.github.com/spec/v1') + expect(page).not_to have_content('oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897') + expect(page).not_to have_content('size 1575078') + + page.within('.content') do + expect(page).to have_content('Delete') + expect(page).to have_content('History') + expect(page).to have_content('Permalink') + expect(page).to have_content('Replace') + expect(page).to have_link('Download') + + expect(page).not_to have_content('Annotate') + expect(page).not_to have_content('Blame') + + expect(page).not_to have_selector(:link_or_button, text: /^Edit$/) + expect(page).to have_selector(:link_or_button, 'Open in Web IDE') + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/user_deletes_files_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/user_deletes_files_spec.rb new file mode 100644 index 00000000000..d503c9b1192 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/user_deletes_files_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Files > User deletes files', :js do + let(:fork_message) do + "You're not allowed to make changes to this project directly. "\ + "A fork of this project has been created that you can make changes in, so you can submit a merge request." + end + + let(:project) { create(:project, :repository, name: 'Shop') } + let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } + let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } + let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) } + let(:user) { create(:user) } + + before do + stub_feature_flags(refactor_blob_viewer: false) + sign_in(user) + end + + context 'when an user has write access' do + before do + project.add_maintainer(user) + visit(project_tree_path_root_ref) + wait_for_requests + end + + it 'deletes the file', :js do + click_link('.gitignore') + + expect(page).to have_content('.gitignore') + + click_on('Delete') + fill_in(:commit_message, with: 'New commit message', visible: true) + click_button('Delete file') + + expect(page).to have_current_path(project_tree_path(project, 'master/'), ignore_query: true) + expect(page).not_to have_content('.gitignore') + end + end + + context 'when an user does not have write access', :js do + before do + project2.add_reporter(user) + visit(project2_tree_path_root_ref) + wait_for_requests + end + + it 'deletes the file in a forked project', :js, :sidekiq_might_not_need_inline do + click_link('.gitignore') + + expect(page).to have_content('.gitignore') + + click_on('Delete') + + expect(page).to have_link('Fork') + expect(page).to have_button('Cancel') + + click_link('Fork') + + expect(page).to have_content(fork_message) + + click_on('Delete') + fill_in(:commit_message, with: 'New commit message', visible: true) + click_button('Delete file') + + fork = user.fork_of(project2.reload) + + expect(page).to have_current_path(project_new_merge_request_path(fork), ignore_query: true) + expect(page).to have_content('New commit message') + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/user_edits_files_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/user_edits_files_spec.rb new file mode 100644 index 00000000000..7a70d67d8ca --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/user_edits_files_spec.rb @@ -0,0 +1,226 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Files > User edits files', :js do + include ProjectForksHelper + include BlobSpecHelpers + + let(:project) { create(:project, :repository, name: 'Shop') } + let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } + let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } + let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) } + let(:user) { create(:user) } + + before do + stub_feature_flags(refactor_blob_viewer: false) + sign_in(user) + end + + after do + unset_default_button + end + + shared_examples 'unavailable for an archived project' do + it 'does not show the edit link for an archived project', :js do + project.update!(archived: true) + visit project_tree_path(project, project.repository.root_ref) + + click_link('.gitignore') + + aggregate_failures 'available edit buttons' do + expect(page).not_to have_text('Edit') + expect(page).not_to have_text('Web IDE') + + expect(page).not_to have_text('Replace') + expect(page).not_to have_text('Delete') + end + end + end + + context 'when an user has write access', :js do + before do + project.add_maintainer(user) + visit(project_tree_path_root_ref) + wait_for_requests + end + + it 'inserts a content of a file' do + set_default_button('edit') + click_link('.gitignore') + click_link_or_button('Edit') + find('.file-editor', match: :first) + + find('#editor') + set_editor_value('*.rbca') + + expect(editor_value).to eq('*.rbca') + end + + it 'does not show the edit link if a file is binary' do + binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png') + visit(project_blob_path(project, binary_file)) + wait_for_requests + + page.within '.content' do + expect(page).not_to have_link('edit') + end + end + + it 'commits an edited file' do + set_default_button('edit') + click_link('.gitignore') + click_link_or_button('Edit') + find('.file-editor', match: :first) + + find('#editor') + set_editor_value('*.rbca') + fill_in(:commit_message, with: 'New commit message', visible: true) + click_button('Commit changes') + + expect(page).to have_current_path(project_blob_path(project, 'master/.gitignore'), ignore_query: true) + + wait_for_requests + + expect(page).to have_content('*.rbca') + end + + it 'commits an edited file to a new branch' do + set_default_button('edit') + click_link('.gitignore') + click_link_or_button('Edit') + + find('.file-editor', match: :first) + + find('#editor') + set_editor_value('*.rbca') + fill_in(:commit_message, with: 'New commit message', visible: true) + fill_in(:branch_name, with: 'new_branch_name', visible: true) + click_button('Commit changes') + + expect(page).to have_current_path(project_new_merge_request_path(project), ignore_query: true) + + click_link('Changes') + + expect(page).to have_content('*.rbca') + end + + it 'shows the diff of an edited file' do + set_default_button('edit') + click_link('.gitignore') + click_link_or_button('Edit') + find('.file-editor', match: :first) + + find('#editor') + set_editor_value('*.rbca') + click_link('Preview changes') + + expect(page).to have_css('.line_holder.new') + end + + it_behaves_like 'unavailable for an archived project' + end + + context 'when an user does not have write access', :js do + before do + project2.add_reporter(user) + visit(project2_tree_path_root_ref) + wait_for_requests + end + + def expect_fork_prompt + expect(page).to have_selector(:link_or_button, 'Fork') + expect(page).to have_selector(:link_or_button, 'Cancel') + expect(page).to have_content( + "You can’t edit files directly in this project. "\ + "Fork this project and submit a merge request with your changes." + ) + end + + def expect_fork_status + expect(page).to have_content( + "You're not allowed to make changes to this project directly. "\ + "A fork of this project has been created that you can make changes in, so you can submit a merge request." + ) + end + + it 'inserts a content of a file in a forked project', :sidekiq_might_not_need_inline do + set_default_button('edit') + click_link('.gitignore') + click_link_or_button('Edit') + + expect_fork_prompt + + click_link_or_button('Fork project') + + expect_fork_status + + find('.file-editor', match: :first) + + find('#editor') + set_editor_value('*.rbca') + + expect(editor_value).to eq('*.rbca') + end + + it 'commits an edited file in a forked project', :sidekiq_might_not_need_inline do + set_default_button('edit') + click_link('.gitignore') + click_link_or_button('Edit') + + expect_fork_prompt + click_link_or_button('Fork project') + + find('.file-editor', match: :first) + + find('#editor') + set_editor_value('*.rbca') + fill_in(:commit_message, with: 'New commit message', visible: true) + click_button('Commit changes') + + fork = user.fork_of(project2.reload) + + expect(page).to have_current_path(project_new_merge_request_path(fork), ignore_query: true) + + wait_for_requests + + expect(page).to have_content('New commit message') + end + + context 'when the user already had a fork of the project', :js do + let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } + + before do + visit(project2_tree_path_root_ref) + wait_for_requests + end + + it 'links to the forked project for editing', :sidekiq_might_not_need_inline do + set_default_button('edit') + click_link('.gitignore') + click_link_or_button('Edit') + + expect(page).not_to have_link('Fork project') + + find('#editor') + set_editor_value('*.rbca') + fill_in(:commit_message, with: 'Another commit', visible: true) + click_button('Commit changes') + + fork = user.fork_of(project2) + + expect(page).to have_current_path(project_new_merge_request_path(fork), ignore_query: true) + + wait_for_requests + + expect(page).to have_content('Another commit') + expect(page).to have_content("From #{forked_project.full_path}") + expect(page).to have_content("into #{project2.full_path}") + end + + it_behaves_like 'unavailable for an archived project' do + let(:project) { project2 } + end + end + end +end diff --git a/spec/features/refactor_blob_viewer_disabled/projects/files/user_replaces_files_spec.rb b/spec/features/refactor_blob_viewer_disabled/projects/files/user_replaces_files_spec.rb new file mode 100644 index 00000000000..5561cf15a66 --- /dev/null +++ b/spec/features/refactor_blob_viewer_disabled/projects/files/user_replaces_files_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Files > User replaces files', :js do + include DropzoneHelper + + let(:fork_message) do + "You're not allowed to make changes to this project directly. "\ + "A fork of this project has been created that you can make changes in, so you can submit a merge request." + end + + let(:project) { create(:project, :repository, name: 'Shop') } + let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } + let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } + let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) } + let(:user) { create(:user) } + + before do + stub_feature_flags(refactor_blob_viewer: false) + sign_in(user) + end + + context 'when an user has write access' do + before do + project.add_maintainer(user) + visit(project_tree_path_root_ref) + wait_for_requests + end + + it 'replaces an existed file with a new one' do + click_link('.gitignore') + + expect(page).to have_content('.gitignore') + + click_on('Replace') + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + + page.within('#modal-upload-blob') do + fill_in(:commit_message, with: 'Replacement file commit message') + end + + click_button('Replace file') + + expect(page).to have_content('Lorem ipsum dolor sit amet') + expect(page).to have_content('Sed ut perspiciatis unde omnis') + expect(page).to have_content('Replacement file commit message') + end + end + + context 'when an user does not have write access' do + before do + project2.add_reporter(user) + visit(project2_tree_path_root_ref) + wait_for_requests + end + + it 'replaces an existed file with a new one in a forked project', :sidekiq_might_not_need_inline do + click_link('.gitignore') + + expect(page).to have_content('.gitignore') + + click_on('Replace') + + expect(page).to have_link('Fork') + expect(page).to have_button('Cancel') + + click_link('Fork') + + expect(page).to have_content(fork_message) + + click_on('Replace') + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + + page.within('#modal-upload-blob') do + fill_in(:commit_message, with: 'Replacement file commit message') + end + + click_button('Replace file') + + expect(page).to have_content('Replacement file commit message') + + fork = user.fork_of(project2.reload) + + expect(page).to have_current_path(project_new_merge_request_path(fork), ignore_query: true) + + click_link('Changes') + + expect(page).to have_content('Lorem ipsum dolor sit amet') + expect(page).to have_content('Sed ut perspiciatis unde omnis') + end + end +end diff --git a/spec/frontend/pipelines/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/pipelines_ci_templates_spec.js index db66b675fb9..ea64788ea47 100644 --- a/spec/frontend/pipelines/pipelines_ci_templates_spec.js +++ b/spec/frontend/pipelines/pipelines_ci_templates_spec.js @@ -1,7 +1,19 @@ import '~/commons'; -import { shallowMount } from '@vue/test-utils'; +import { GlButton, GlSprintf } from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mockTracking } from 'helpers/tracking_helper'; +import { stubExperiments } from 'helpers/experimentation_helper'; +import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; +import ExperimentTracking from '~/experimentation/experiment_tracking'; import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue'; +import { + RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME, + RUNNERS_SETTINGS_LINK_CLICKED_EVENT, + RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT, + RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT, + I18N, +} from '~/pipeline_editor/constants'; const pipelineEditorPath = '/-/ci/editor'; const suggestedCiTemplates = [ @@ -10,16 +22,20 @@ const suggestedCiTemplates = [ { name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' }, ]; +jest.mock('~/experimentation/experiment_tracking'); + describe('Pipelines CI Templates', () => { let wrapper; let trackingSpy; - const createWrapper = () => { - return shallowMount(PipelinesCiTemplate, { + const createWrapper = (propsData = {}, stubs = {}) => { + return shallowMountExtended(PipelinesCiTemplate, { provide: { pipelineEditorPath, suggestedCiTemplates, }, + propsData, + stubs, }); }; @@ -28,6 +44,9 @@ describe('Pipelines CI Templates', () => { const findTemplateLinks = () => wrapper.findAll('[data-testid="template-link"]'); const findTemplateNames = () => wrapper.findAll('[data-testid="template-name"]'); const findTemplateLogos = () => wrapper.findAll('[data-testid="template-logo"]'); + const findSettingsLink = () => wrapper.findByTestId('settings-link'); + const findDocumentationLink = () => wrapper.findByTestId('documentation-link'); + const findSettingsButton = () => wrapper.findByTestId('settings-button'); afterEach(() => { wrapper.destroy(); @@ -69,7 +88,7 @@ describe('Pipelines CI Templates', () => { it('has the description of the template', () => { expect(findTemplateDescriptions().at(0).text()).toBe( - 'CI/CD template to test and deploy your Android project.', + sprintf(I18N.templates.description, { name: 'Android' }), ); }); @@ -104,4 +123,73 @@ describe('Pipelines CI Templates', () => { }); }); }); + + describe('when the runners_availability_section experiment is active', () => { + beforeEach(() => { + stubExperiments({ runners_availability_section: 'candidate' }); + }); + + describe('when runners are available', () => { + beforeEach(() => { + wrapper = createWrapper({ anyRunnersAvailable: true }, { GitlabExperiment, GlSprintf }); + }); + + it('renders the templates', () => { + expect(findTestTemplateLinks().exists()).toBe(true); + expect(findTemplateLinks().exists()).toBe(true); + }); + + it('show the runners available section', () => { + expect(wrapper.text()).toContain(I18N.runners.title); + }); + + it('tracks an event when clicking the settings link', () => { + findSettingsLink().vm.$emit('click'); + + expect(ExperimentTracking).toHaveBeenCalledWith( + RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME, + ); + expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith( + RUNNERS_SETTINGS_LINK_CLICKED_EVENT, + ); + }); + + it('tracks an event when clicking the documentation link', () => { + findDocumentationLink().vm.$emit('click'); + + expect(ExperimentTracking).toHaveBeenCalledWith( + RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME, + ); + expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith( + RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT, + ); + }); + }); + + describe('when runners are not available', () => { + beforeEach(() => { + wrapper = createWrapper({ anyRunnersAvailable: false }, { GitlabExperiment, GlButton }); + }); + + it('does not render the templates', () => { + expect(findTestTemplateLinks().exists()).toBe(false); + expect(findTemplateLinks().exists()).toBe(false); + }); + + it('show the no runners available section', () => { + expect(wrapper.text()).toContain(I18N.noRunners.title); + }); + + it('tracks an event when clicking the settings button', () => { + findSettingsButton().trigger('click'); + + expect(ExperimentTracking).toHaveBeenCalledWith( + RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME, + ); + expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith( + RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT, + ); + }); + }); + }); }); diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb index 751bcc97582..1baaefe0b7f 100644 --- a/spec/helpers/ci/pipelines_helper_spec.rb +++ b/spec/helpers/ci/pipelines_helper_spec.rb @@ -93,4 +93,63 @@ RSpec.describe Ci::PipelinesHelper do end end end + + describe '#pipelines_list_data' do + let_it_be(:project) { create(:project) } + + subject(:data) { helper.pipelines_list_data(project, 'list_url') } + + before do + allow(helper).to receive(:can?).and_return(true) + end + + it 'has the expected keys' do + expect(subject.keys).to match_array([:endpoint, + :project_id, + :params, + :artifacts_endpoint, + :artifacts_endpoint_placeholder, + :pipeline_schedule_url, + :empty_state_svg_path, + :error_state_svg_path, + :no_pipelines_svg_path, + :can_create_pipeline, + :new_pipeline_path, + :ci_lint_path, + :reset_cache_path, + :has_gitlab_ci, + :pipeline_editor_path, + :suggested_ci_templates, + :code_quality_page_path, + :ci_runner_settings_path]) + end + + describe 'the `any_runners_available` attribute' do + subject { data[:any_runners_available] } + + context 'when the `runners_availability_section` experiment variant is control' do + before do + stub_experiments(runners_availability_section: :control) + end + + it { is_expected.to be_nil } + end + + context 'when the `runners_availability_section` experiment variant is candidate' do + before do + stub_experiments(runners_availability_section: :candidate) + end + + context 'when there are no runners' do + it { is_expected.to eq('false') } + end + + context 'when there are runners' do + let!(:runner) { create(:ci_runner, :project, projects: [project]) } + + it { is_expected.to eq('true') } + end + end + end + end end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index cf3239da5f5..1cc4e69b3d9 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -112,11 +112,18 @@ RSpec.describe Gitlab do expect(described_class.com?).to eq false end - it 'is true when GITLAB_SIMULATE_SAAS is true' do + it 'is true when GITLAB_SIMULATE_SAAS is true and in development' do + stub_rails_env('development') stub_env('GITLAB_SIMULATE_SAAS', '1') expect(described_class.com?).to eq true end + + it 'is false when GITLAB_SIMULATE_SAAS is true and in test' do + stub_env('GITLAB_SIMULATE_SAAS', '1') + + expect(described_class.com?).to eq false + end end describe '.com' do @@ -239,8 +246,8 @@ RSpec.describe Gitlab do stub_env('GITLAB_SIMULATE_SAAS', '1') end - it 'is true when test env' do - expect(subject).to eq true + it 'is false when test env' do + expect(subject).to eq false end it 'is true when dev env' do @@ -249,7 +256,7 @@ RSpec.describe Gitlab do expect(subject).to eq true end - it 'is false when env is not dev or test' do + it 'is false when env is not dev' do stub_rails_env('production') expect(subject).to eq false